io.mjs

import { createHttpRequest, BIT, internals, settings, browser,
         create, getMethods, addMethods, isNodeJs, isObject, isFunc, isStr,
         clTObject, clTNamed, clTString, clTObjString, clTKey, clTFile, clTTree, clTList, clTMap, clTObjArray, clTClonesArray,
         clTAttLine, clTAttFill, clTAttMarker, clTStyle, clTImagePalette, atob_func,
         clTPad, clTCanvas, clTAttCanvas, clTPolyMarker3D, clTF1, clTF12, clTF2, clTF3 } from './core.mjs';

const clTStreamerElement = 'TStreamerElement', clTStreamerObject = 'TStreamerObject',
      clTStreamerSTL = 'TStreamerSTL', clTStreamerInfoList = 'TStreamerInfoList',
      clTDirectory = 'TDirectory', clTDirectoryFile = 'TDirectoryFile',
      clTQObject = 'TQObject', clTBasket = 'TBasket', clTDatime = 'TDatime',
      nameStreamerInfo = 'StreamerInfo',

      kChar = 1, kShort = 2, kInt = 3, kLong = 4, kFloat = 5, kCounter = 6,
      kCharStar = 7, kDouble = 8, kDouble32 = 9, kLegacyChar = 10,
      kUChar = 11, kUShort = 12, kUInt = 13, kULong = 14, kBits = 15,
      kLong64 = 16, kULong64 = 17, kBool = 18, kFloat16 = 19,

      kBase = 0, kOffsetL = 20, kOffsetP = 40,
      kObject = 61, kAny = 62, kObjectp = 63, kObjectP = 64, kTString = 65,
      kTObject = 66, kTNamed = 67, kAnyp = 68, kAnyP = 69,

      /* kAnyPnoVT: 70, */
      kSTLp = 71,
      /* kSkip = 100, kSkipL = 120, kSkipP = 140, kConv = 200, kConvL = 220, kConvP = 240, */
      kSTL = 300, /* kSTLstring = 365, */

      kStreamer = 500, kStreamLoop = 501,

      kMapOffset = 2, kByteCountMask = 0x40000000, kNewClassTag = 0xFFFFFFFF, kClassMask = 0x80000000,

      // constants of bits in version
      kStreamedMemberWise = BIT(14),

      // constants used for coding type of STL container
      kNotSTL = 0, kSTLvector = 1, kSTLlist = 2, kSTLdeque = 3, kSTLmap = 4, kSTLmultimap = 5,
      kSTLset = 6, kSTLmultiset = 7, kSTLbitset = 8,
      // kSTLforwardlist = 9, kSTLunorderedset = 10, kSTLunorderedmultiset = 11, kSTLunorderedmap = 12,
      // kSTLunorderedmultimap = 13, kSTLend = 14
      kBaseClass = 'BASE',

      // name of base IO types
      BasicTypeNames = [kBaseClass, 'char', 'short', 'int', 'long', 'float', 'int', 'const char*', 'double', 'Double32_t',
                        'char', 'unsigned  char', 'unsigned short', 'unsigned', 'unsigned long', 'unsigned', 'Long64_t', 'ULong64_t', 'bool', 'Float16_t'],

      // names of STL containers
      StlNames = ['', 'vector', 'list', 'deque', 'map', 'multimap', 'set', 'multiset', 'bitset'],

      // TObject bits
      kIsReferenced = BIT(4), kHasUUID = BIT(5);


/** @summary Custom streamers for root classes
  * @desc map of user-streamer function like func(buf,obj)
  * or alias (classname) which can be used to read that function
  * or list of read functions
  * @private */
/* eslint-disable one-var */
const CustomStreamers = {
   TObject(buf, obj) {
      obj.fUniqueID = buf.ntou4();
      obj.fBits = buf.ntou4();
      if (obj.fBits & kIsReferenced)
         buf.ntou2(); // skip pid
   },

   TNamed: [
      {
         basename: clTObject, base: 1,
         func(buf, obj) {
            if (!obj._typename)
               obj._typename = clTNamed;
            buf.classStreamer(obj, clTObject);
         }
      },
      { name: 'fName', func(buf, obj) { obj.fName = buf.readTString(); } },
      { name: 'fTitle', func(buf, obj) { obj.fTitle = buf.readTString(); } }
   ],

   TObjString: [
      {
         basename: clTObject, base: 1,
         func(buf, obj) {
            if (!obj._typename)
               obj._typename = clTObjString;
            buf.classStreamer(obj, clTObject);
         }
      },
      { name: 'fString', func(buf, obj) { obj.fString = buf.readTString(); } }
   ],

   TClonesArray(buf, list) {
      if (!list._typename)
         list._typename = clTClonesArray;
      list.$kind = clTClonesArray;
      list.name = '';
      const ver = buf.last_read_version;
      if (ver > 2)
         buf.classStreamer(list, clTObject);
      if (ver > 1)
         list.name = buf.readTString();
      let classv = buf.readTString(), clv = 0;
      const pos = classv.lastIndexOf(';');

      if (pos > 0) {
         clv = Number.parseInt(classv.slice(pos + 1));
         classv = classv.slice(0, pos);
      }

      let nobjects = buf.ntou4();
      if (nobjects < 0)
         nobjects = -nobjects;  // for backward compatibility

      list.arr = new Array(nobjects);
      list.fLast = nobjects - 1;
      list.fLowerBound = buf.ntou4();

      let streamer = buf.fFile.getStreamer(classv, { val: clv });
      streamer = buf.fFile.getSplittedStreamer(streamer);

      if (!streamer)
         console.log(`Cannot get member-wise streamer for ${classv}:${clv}`);
      else {
         // create objects
         for (let n = 0; n < nobjects; ++n)
            list.arr[n] = { _typename: classv };

         // call streamer for all objects member-wise
         for (let k = 0; k < streamer.length; ++k) {
            for (let n = 0; n < nobjects; ++n)
               streamer[k].func(buf, list.arr[n]);
         }
      }
   },

   TMap(buf, map) {
      if (!map._typename)
         map._typename = clTMap;
      map.name = '';
      map.arr = [];
      const ver = buf.last_read_version;
      if (ver > 2)
         buf.classStreamer(map, clTObject);
      if (ver > 1)
         map.name = buf.readTString();

      const nobjects = buf.ntou4();
      // create objects
      for (let n = 0; n < nobjects; ++n) {
         const obj = { _typename: 'TPair' };
         obj.first = buf.readObjectAny();
         obj.second = buf.readObjectAny();
         if (obj.first)
            map.arr.push(obj);
      }
   },

   TTreeIndex(buf, obj) {
      const ver = buf.last_read_version;
      obj._typename = 'TTreeIndex';
      buf.classStreamer(obj, 'TVirtualIndex');
      obj.fMajorName = buf.readTString();
      obj.fMinorName = buf.readTString();
      obj.fN = buf.ntoi8();
      obj.fIndexValues = buf.readFastArray(obj.fN, kLong64);
      if (ver > 1)
         obj.fIndexValuesMinor = buf.readFastArray(obj.fN, kLong64);
      obj.fIndex = buf.readFastArray(obj.fN, kLong64);
   },

   TRefArray(buf, obj) {
      obj._typename = 'TRefArray';
      buf.classStreamer(obj, clTObject);
      obj.name = buf.readTString();
      const nobj = buf.ntoi4();
      obj.fLast = nobj - 1;
      obj.fLowerBound = buf.ntoi4();
      /* const pidf = */ buf.ntou2();
      obj.fUIDs = buf.readFastArray(nobj, kUInt);
   },

   TCanvas(buf, obj) {
      obj._typename = clTCanvas;
      buf.classStreamer(obj, clTPad);
      obj.fDISPLAY = buf.readTString();
      obj.fDoubleBuffer = buf.ntoi4();
      obj.fRetained = buf.ntobool();
      obj.fXsizeUser = buf.ntoi4();
      obj.fYsizeUser = buf.ntoi4();
      obj.fXsizeReal = buf.ntoi4();
      obj.fYsizeReal = buf.ntoi4();
      obj.fWindowTopX = buf.ntoi4();
      obj.fWindowTopY = buf.ntoi4();
      obj.fWindowWidth = buf.ntoi4();
      obj.fWindowHeight = buf.ntoi4();
      obj.fCw = buf.ntou4();
      obj.fCh = buf.ntou4();
      obj.fCatt = buf.classStreamer({}, clTAttCanvas);
      buf.ntou1(); // ignore b << TestBit(kMoveOpaque);
      buf.ntou1(); // ignore b << TestBit(kResizeOpaque);
      obj.fHighLightColor = buf.ntoi2();
      obj.fBatch = buf.ntobool();
      buf.ntou1();   // ignore b << TestBit(kShowEventStatus);
      buf.ntou1();   // ignore b << TestBit(kAutoExec);
      buf.ntou1();   // ignore b << TestBit(kMenuBar);
   },

   TObjArray(buf, list) {
      if (!list._typename)
         list._typename = clTObjArray;
      list.$kind = clTObjArray;
      list.name = '';
      const ver = buf.last_read_version;
      if (ver > 2)
         buf.classStreamer(list, clTObject);
      if (ver > 1)
         list.name = buf.readTString();
      const nobjects = buf.ntou4();
      let i = 0;
      list.arr = new Array(nobjects);
      list.fLast = nobjects - 1;
      list.fLowerBound = buf.ntou4();
      while (i < nobjects)
         list.arr[i++] = buf.readObjectAny();
   },

   TPolyMarker3D(buf, marker) {
      const ver = buf.last_read_version;
      buf.classStreamer(marker, clTObject);
      buf.classStreamer(marker, clTAttMarker);
      marker.fN = buf.ntoi4();
      marker.fP = buf.readFastArray(marker.fN * 3, kFloat);
      marker.fOption = buf.readTString();
      marker.fName = (ver > 1) ? buf.readTString() : clTPolyMarker3D;
   },

   TPolyLine3D(buf, obj) {
      buf.classStreamer(obj, clTObject);
      buf.classStreamer(obj, clTAttLine);
      obj.fN = buf.ntoi4();
      obj.fP = buf.readFastArray(obj.fN * 3, kFloat);
      obj.fOption = buf.readTString();
   },

   TStreamerInfo(buf, obj) {
      buf.classStreamer(obj, clTNamed);
      obj.fCheckSum = buf.ntou4();
      obj.fClassVersion = buf.ntou4();
      obj.fElements = buf.readObjectAny();
   },

   TStreamerElement(buf, element) {
      const ver = buf.last_read_version;
      buf.classStreamer(element, clTNamed);
      element.fType = buf.ntou4();
      element.fSize = buf.ntou4();
      element.fArrayLength = buf.ntou4();
      element.fArrayDim = buf.ntou4();
      element.fMaxIndex = buf.readFastArray((ver === 1) ? buf.ntou4() : 5, kUInt);
      element.fTypeName = buf.readTString();

      if ((element.fType === kUChar) && ((element.fTypeName === 'Bool_t') || (element.fTypeName === 'bool')))
         element.fType = kBool;

      element.fXmin = element.fXmax = element.fFactor = 0;
      if (ver === 3) {
         element.fXmin = buf.ntod();
         element.fXmax = buf.ntod();
         element.fFactor = buf.ntod();
      } else if ((ver > 3) && (element.fBits & BIT(6))) { // kHasRange
         let p1 = element.fTitle.indexOf('[');
         if ((p1 >= 0) && (element.fType > kOffsetP))
            p1 = element.fTitle.indexOf('[', p1 + 1);
         const p2 = element.fTitle.indexOf(']', p1 + 1);

         if ((p1 >= 0) && (p2 >= p1 + 2)) {
            const arr = element.fTitle.slice(p1 + 1, p2).split(',');
            let nbits = 32;
            if (!arr || arr.length < 2)
               throw new Error(`Problem to decode range setting from streamer element title ${element.fTitle}`);

            if (arr.length === 3)
               nbits = parseInt(arr[2]);
            if (!Number.isInteger(nbits) || (nbits < 2) || (nbits > 32))
               nbits = 32;

            const parse_range = val => {
               if (!val)
                  return 0;
               if (val.indexOf('pi') < 0)
                  return parseFloat(val);
               val = val.trim();
               let sign = 1;
               if (val[0] === '-') {
                  sign = -1;
                  val = val.slice(1);
               }
               switch (val) {
                  case '2pi':
                  case '2*pi':
                  case 'twopi': return sign * 2 * Math.PI;
                  case 'pi/2': return sign * Math.PI / 2;
                  case 'pi/4': return sign * Math.PI / 4;
               }
               return sign * Math.PI;
            };

            element.fXmin = parse_range(arr[0]);
            element.fXmax = parse_range(arr[1]);

            // avoid usage of 1 << nbits, while only works up to 32 bits
            const bigint = ((nbits >= 0) && (nbits < 32)) ? Math.pow(2, nbits) : 0xffffffff;
            if (element.fXmin < element.fXmax)
               element.fFactor = bigint / (element.fXmax - element.fXmin);
            else if (nbits < 15)
               element.fXmin = nbits;
         }
      }
   },

   TStreamerBase(buf, elem) {
      const ver = buf.last_read_version;
      buf.classStreamer(elem, clTStreamerElement);
      if (ver > 2)
         elem.fBaseVersion = buf.ntou4();
   },

   TStreamerSTL(buf, elem) {
      buf.classStreamer(elem, clTStreamerElement);
      elem.fSTLtype = buf.ntou4();
      elem.fCtype = buf.ntou4();

      if ((elem.fSTLtype === kSTLmultimap) &&
          ((elem.fTypeName.indexOf('std::set') === 0) || (elem.fTypeName.indexOf('set') === 0)))
         elem.fSTLtype = kSTLset;

      if ((elem.fSTLtype === kSTLset) &&
          ((elem.fTypeName.indexOf('std::multimap') === 0) || (elem.fTypeName.indexOf('multimap') === 0)))
         elem.fSTLtype = kSTLmultimap;
   },

   TStreamerSTLstring(buf, elem) {
      if (buf.last_read_version > 0)
         buf.classStreamer(elem, clTStreamerSTL);
   },

   TList(buf, obj) {
      // stream all objects in the list from the I/O buffer
      if (!obj._typename)
         obj._typename = this.typename;
      obj.$kind = clTList; // all derived classes will be marked as well
      if (buf.last_read_version > 3) {
         buf.classStreamer(obj, clTObject);
         obj.name = buf.readTString();
         const nobjects = buf.ntou4();
         obj.arr = new Array(nobjects);
         obj.opt = new Array(nobjects);
         for (let i = 0; i < nobjects; ++i) {
            obj.arr[i] = buf.readObjectAny();
            obj.opt[i] = buf.readTString();
         }
      } else {
         obj.name = '';
         obj.arr = [];
         obj.opt = [];
      }
   },

   THashList: clTList,

   TStreamerLoop(buf, elem) {
      if (buf.last_read_version > 1) {
         buf.classStreamer(elem, clTStreamerElement);
         elem.fCountVersion = buf.ntou4();
         elem.fCountName = buf.readTString();
         elem.fCountClass = buf.readTString();
      }
   },

   TStreamerBasicPointer: 'TStreamerLoop',

   TStreamerObject(buf, elem) {
      if (buf.last_read_version > 1)
         buf.classStreamer(elem, clTStreamerElement);
   },

   TStreamerBasicType: clTStreamerObject,
   TStreamerObjectAny: clTStreamerObject,
   TStreamerString: clTStreamerObject,
   TStreamerObjectPointer: clTStreamerObject,

   TStreamerObjectAnyPointer(buf, elem) {
      if (buf.last_read_version > 0)
         buf.classStreamer(elem, clTStreamerElement);
   },

   TTree: {
      name: '$file',
      func(buf, obj) {
         obj.$kind = clTTree;
         obj.$file = buf.fFile;
      }
   },

   TBranch(buf, obj) {
      const v = buf.last_read_version;
      if (v > 9)
         buf.streamClassMembers(obj, 'TBranch', v);
      else {
         buf.classStreamer(obj, clTNamed);
         if (v > 7)
            buf.classStreamer(obj, clTAttFill);
         obj.fCompress = buf.ntoi4();
         obj.fBasketSize = buf.ntoi4();
         obj.fEntryOffsetLen = buf.ntoi4();
         obj.fWriteBasket = buf.ntoi4();
         obj.fEntryNumber = buf.ntoi4();
         obj.fOffset = buf.ntoi4();
         obj.fMaxBaskets = buf.ntoi4();
         if (v > 6)
            obj.fSplitLevel = buf.ntoi4();
         obj.fEntries = buf.ntod();
         obj.fTotBytes = buf.ntod();
         obj.fZipBytes = buf.ntod();
         obj.fBranches = buf.classStreamer({}, clTObjArray);
         obj.fLeaves = buf.classStreamer({}, clTObjArray);
         obj.fBaskets = buf.classStreamer({}, clTObjArray);
         buf.ntoi1(); // isArray
         obj.fBasketBytes = buf.readFastArray(obj.fMaxBaskets, kInt);
         buf.ntoi1(); // isArray
         obj.fBasketEntry = buf.readFastArray(obj.fMaxBaskets, kInt);
         const isArray = buf.ntoi1();
         obj.fBasketSeek = buf.readFastArray(obj.fMaxBaskets, isArray === 2 ? kLong64 : kInt);
         obj.fFileName = buf.readTString();
      }
   },

   'ROOT::RNTuple': {
      name: '$file',
      func(buf, obj) {
         obj.$kind = 'ROOT::RNTuple';
         obj.$file = buf.fFile;
      }
   },

   RooRealVar(buf, obj) {
      const v = buf.last_read_version;
      buf.classStreamer(obj, 'RooAbsRealLValue');
      if (v === 1) {
         // skip fitMin, fitMax, fitBins
         buf.ntod();
         buf.ntod();
         buf.ntoi4();
      }
      obj._error = buf.ntod();
      obj._asymErrLo = buf.ntod();
      obj._asymErrHi = buf.ntod();
      if (v >= 2)
         obj._binning = buf.readObjectAny();
      if (v === 3)
         obj._sharedProp = buf.readObjectAny();
      if (v >= 4)
         obj._sharedProp = buf.classStreamer({}, 'RooRealVarSharedProperties');
   },

   RooAbsBinning(buf, obj) {
      buf.classStreamer(obj, (buf.last_read_version === 1) ? clTObject : clTNamed);
      buf.classStreamer(obj, 'RooPrintable');
   },

   RooCategory(buf, obj) {
      const v = buf.last_read_version;
      buf.classStreamer(obj, 'RooAbsCategoryLValue');
      obj._sharedProp = (v === 1) ? buf.readObjectAny() : buf.classStreamer({}, 'RooCategorySharedProperties');
   },

   'RooWorkspace::CodeRepo': (buf /* , obj */) => {
      const sz = (buf.last_read_version === 2) ? 3 : 2;
      for (let i = 0; i < sz; ++i) {
         let cnt = buf.ntoi4() * ((i === 0) ? 4 : 3);
         while (cnt--)
            buf.readTString();
      }
   },

   RooLinkedList(buf, obj) {
      const v = buf.last_read_version;
      buf.classStreamer(obj, clTObject);
      let size = buf.ntoi4();
      obj.arr = create(clTList);
      while (size--)
         obj.arr.Add(buf.readObjectAny());
      if (v > 1)
         obj._name = buf.readTString();
   },

   TImagePalette: [
      {
         basename: clTObject, base: 1, func(buf, obj) {
            if (!obj._typename)
               obj._typename = clTImagePalette;
            buf.classStreamer(obj, clTObject);
         }
      },
      { name: 'fNumPoints', func(buf, obj) { obj.fNumPoints = buf.ntou4(); } },
      { name: 'fPoints', func(buf, obj) { obj.fPoints = buf.readFastArray(obj.fNumPoints, kDouble); } },
      { name: 'fColorRed', func(buf, obj) { obj.fColorRed = buf.readFastArray(obj.fNumPoints, kUShort); } },
      { name: 'fColorGreen', func(buf, obj) { obj.fColorGreen = buf.readFastArray(obj.fNumPoints, kUShort); } },
      { name: 'fColorBlue', func(buf, obj) { obj.fColorBlue = buf.readFastArray(obj.fNumPoints, kUShort); } },
      { name: 'fColorAlpha', func(buf, obj) { obj.fColorAlpha = buf.readFastArray(obj.fNumPoints, kUShort); } }
   ],

   TAttImage: [
      { name: 'fImageQuality', func(buf, obj) { obj.fImageQuality = buf.ntoi4(); } },
      { name: 'fImageCompression', func(buf, obj) { obj.fImageCompression = buf.ntou4(); } },
      { name: 'fConstRatio', func(buf, obj) { obj.fConstRatio = buf.ntobool(); } },
      { name: 'fPalette', func(buf, obj) { obj.fPalette = buf.classStreamer({}, clTImagePalette); } }
   ],

   TASImage(buf, obj) {
      if ((buf.last_read_version === 1) && (buf.fFile.fVersion > 0) && (buf.fFile.fVersion < 50000))
         return console.warn('old TASImage version - not yet supported');

      buf.classStreamer(obj, clTNamed);

      if (buf.ntobool()) {
         const size = buf.ntoi4();
         obj.fPngBuf = buf.readFastArray(size, kUChar);
      } else {
         buf.classStreamer(obj, 'TAttImage');
         obj.fWidth = buf.ntoi4();
         obj.fHeight = buf.ntoi4();
         obj.fImgBuf = buf.readFastArray(obj.fWidth * obj.fHeight, kDouble);
      }
   },

   TMaterial(buf, obj) {
      const v = buf.last_read_version;
      buf.classStreamer(obj, clTNamed);
      obj.fNumber = buf.ntoi4();
      obj.fA = buf.ntof();
      obj.fZ = buf.ntof();
      obj.fDensity = buf.ntof();
      if (v > 2) {
         buf.classStreamer(obj, clTAttFill);
         obj.fRadLength = buf.ntof();
         obj.fInterLength = buf.ntof();
      } else
         obj.fRadLength = obj.fInterLength = 0;
   },

   TMixture(buf, obj) {
      buf.classStreamer(obj, 'TMaterial');
      obj.fNmixt = buf.ntoi4();
      obj.fAmixt = buf.readFastArray(buf.ntoi4(), kFloat);
      obj.fZmixt = buf.readFastArray(buf.ntoi4(), kFloat);
      obj.fWmixt = buf.readFastArray(buf.ntoi4(), kFloat);
   },

   TVirtualPerfStats: clTObject, // use directly TObject streamer

   TMethodCall: clTObject
};


/** @summary Add custom streamer
  * @public */
function addUserStreamer(type, user_streamer) {
   CustomStreamers[type] = user_streamer;
}

/** @summary these are streamers which do not handle version regularly
  * @desc used for special classes like TRef or TBasket
  * @private */
const DirectStreamers = {
   // do nothing for these classes
   TQObject() {},
   TGraphStruct() {},
   TGraphNode() {},
   TGraphEdge() {},

   TDatime(buf, obj) {
      obj.fDatime = buf.ntou4();
   },

   TKey(buf, key) {
      key.fNbytes = buf.ntoi4();
      key.fVersion = buf.ntoi2();
      key.fObjlen = buf.ntou4();
      key.fDatime = buf.classStreamer({}, clTDatime);
      key.fKeylen = buf.ntou2();
      key.fCycle = buf.ntou2();
      if (key.fVersion > 1000) {
         key.fSeekKey = buf.ntou8();
         buf.shift(8); // skip seekPdir
      } else {
         key.fSeekKey = buf.ntou4();
         buf.shift(4); // skip seekPdir
      }
      key.fClassName = buf.readTString();
      key.fName = buf.readTString();
      key.fTitle = buf.readTString();
   },

   TDirectory(buf, dir) {
      const version = buf.ntou2();
      dir.fDatimeC = buf.classStreamer({}, clTDatime);
      dir.fDatimeM = buf.classStreamer({}, clTDatime);
      dir.fNbytesKeys = buf.ntou4();
      dir.fNbytesName = buf.ntou4();
      dir.fSeekDir = (version > 1000) ? buf.ntou8() : buf.ntou4();
      dir.fSeekParent = (version > 1000) ? buf.ntou8() : buf.ntou4();
      dir.fSeekKeys = (version > 1000) ? buf.ntou8() : buf.ntou4();
      // if ((version % 1000) > 2) buf.shift(18); // skip fUUID
   },

   TRef(buf, obj) {
      buf.classStreamer(obj, clTObject);
      if (obj.fBits & kHasUUID)
         obj.fUUID = buf.readTString();
      else
         obj.fPID = buf.ntou2();
   },

   'TMatrixTSym<float>': (buf, obj) => {
      buf.classStreamer(obj, 'TMatrixTBase<float>');
      obj.fElements = new Float32Array(obj.fNelems);
      const arr = buf.readFastArray((obj.fNrows * (obj.fNcols + 1)) / 2, kFloat);
      for (let i = 0, cnt = 0; i < obj.fNrows; ++i) {
         for (let j = i; j < obj.fNcols; ++j)
            obj.fElements[j * obj.fNcols + i] = obj.fElements[i * obj.fNcols + j] = arr[cnt++];
      }
   },

   'TMatrixTSym<double>': (buf, obj) => {
      buf.classStreamer(obj, 'TMatrixTBase<double>');
      obj.fElements = new Float64Array(obj.fNelems);
      const arr = buf.readFastArray((obj.fNrows * (obj.fNcols + 1)) / 2, kDouble);
      for (let i = 0, cnt = 0; i < obj.fNrows; ++i) {
         for (let j = i; j < obj.fNcols; ++j)
            obj.fElements[j * obj.fNcols + i] = obj.fElements[i * obj.fNcols + j] = arr[cnt++];
      }
   }
};

/** @summary Returns type id by its name
  * @private */
function getTypeId(typname, norecursion) {
   switch (typname) {
      case 'bool':
      case 'Bool_t': return kBool;
      case 'char':
      case 'signed char':
      case 'Char_t': return kChar;
      case 'Color_t':
      case 'Style_t':
      case 'Width_t':
      case 'short':
      case 'Short_t': return kShort;
      case 'int':
      case 'EErrorType':
      case 'Int_t': return kInt;
      case 'long':
      case 'Long_t': return kLong;
      case 'float':
      case 'Float_t': return kFloat;
      case 'double':
      case 'Double_t': return kDouble;
      case 'unsigned char':
      case 'UChar_t': return kUChar;
      case 'unsigned short':
      case 'UShort_t': return kUShort;
      case 'unsigned':
      case 'unsigned int':
      case 'UInt_t': return kUInt;
      case 'unsigned long':
      case 'ULong_t': return kULong;
      case 'int64_t':
      case 'long long':
      case 'Long64_t': return kLong64;
      case 'uint64_t':
      case 'unsigned long long':
      case 'ULong64_t': return kULong64;
      case 'Double32_t': return kDouble32;
      case 'Float16_t': return kFloat16;
      case 'char*':
      case 'const char*':
      case 'const Char_t*': return kCharStar;
   }

   if (!norecursion) {
      const replace = CustomStreamers[typname];
      if (isStr(replace))
         return getTypeId(replace, true);
   }

   return -1;
}

/** @summary Analyze and returns arrays kind
  * @return 0 if TString (or equivalent), positive value - some basic type, -1 - any other kind
  * @private */
function getArrayKind(type_name) {
   if ((type_name === clTString) || (type_name === 'string') ||
      (CustomStreamers[type_name] === clTString))
      return 0;
   if ((type_name.length < 7) || type_name.indexOf('TArray'))
      return -1;
   if (type_name.length === 7) {
      switch (type_name[6]) {
         case 'I': return kInt;
         case 'D': return kDouble;
         case 'F': return kFloat;
         case 'S': return kShort;
         case 'C': return kChar;
         case 'L': return kLong;
         default: return -1;
      }
   }

   return type_name === 'TArrayL64' ? kLong64 : -1;
}

// eslint-disable-next-line  prefer-const
let createPairStreamer;

/** @summary create element of the streamer
  * @private  */
function createStreamerElement(name, typename, file) {
   const elem = {
      _typename: clTStreamerElement, fName: name, fTypeName: typename,
      fType: 0, fSize: 0, fArrayLength: 0, fArrayDim: 0, fMaxIndex: [0, 0, 0, 0, 0],
      fXmin: 0, fXmax: 0, fFactor: 0
   };

   if (isStr(typename)) {
      elem.fType = getTypeId(typename);
      if ((elem.fType < 0) && file && file.fBasicTypes[typename])
         elem.fType = file.fBasicTypes[typename];
   } else {
      elem.fType = typename;
      typename = elem.fTypeName = BasicTypeNames[elem.fType] || 'int';
   }

   if (elem.fType > 0)
      return elem; // basic type

   // check if there are STL containers
   const pos = typename.indexOf('<');
   let stltype = kNotSTL;
   if ((pos > 0) && (typename.indexOf('>') > pos + 2)) {
      for (let stl = 1; stl < StlNames.length; ++stl) {
         if (typename.slice(0, pos) === StlNames[stl]) {
            stltype = stl;
            break;
         }
      }
   }

   if (stltype !== kNotSTL) {
      elem._typename = clTStreamerSTL;
      elem.fType = kStreamer;
      elem.fSTLtype = stltype;
      elem.fCtype = 0;
      return elem;
   }

   if ((pos > 0) && (typename.slice(0, pos) === 'pair') && file && isFunc(createPairStreamer))
      createPairStreamer(typename, file);

   const isptr = typename.at(-1) === '*';

   if (isptr)
      elem.fTypeName = typename = typename.slice(0, typename.length - 1);

   if (getArrayKind(typename) === 0) {
      elem.fType = kTString;
      return elem;
   }

   elem.fType = isptr ? kAnyP : kAny;

   return elem;
}

/** @summary Function to read vector element in the streamer
  * @private */
function readVectorElement(buf) {
   if (this.member_wise) {
      const n = buf.ntou4(), ver = this.stl_version;

      if (n === 0)
         return []; // for empty vector no need to search split streamers

      if (n > 1000000)
         throw new Error(`member-wise streaming of ${this.conttype} num ${n} member ${this.name}`);

      let streamer;

      if ((ver.val === this.member_ver) && (ver.checksum === this.member_checksum))
         streamer = this.member_streamer;
      else {
         streamer = buf.fFile.getStreamer(this.conttype, ver);

         this.member_streamer = streamer = buf.fFile.getSplittedStreamer(streamer);
         this.member_ver = ver.val;
         this.member_checksum = ver.checksum;
      }

      const res = new Array(n);
      let i, k, member;

      for (i = 0; i < n; ++i)
         res[i] = { _typename: this.conttype }; // create objects
      if (!streamer)
         console.error(`Fail to create split streamer for ${this.conttype} need to read ${n} objects version ${ver}`);
      else {
         for (k = 0; k < streamer.length; ++k) {
            member = streamer[k];
            if (member.split_func)
               member.split_func(buf, res, n);
            else {
               for (i = 0; i < n; ++i)
                  member.func(buf, res[i]);
            }
         }
      }
      return res;
   }

   const n = buf.ntou4(), res = new Array(n);
   let i = 0;

   if (n > 200000) {
      console.error(`vector streaming for ${this.conttype} at ${n}`);
      return res;
   }

   if (this.arrkind > 0) {
      while (i < n)
         res[i++] = buf.readFastArray(buf.ntou4(), this.arrkind);
   } else if (this.arrkind === 0) {
      while (i < n)
         res[i++] = buf.readTString();
   } else if (this.isptr) {
      while (i < n)
         res[i++] = buf.readObjectAny();
   } else if (this.submember) {
      while (i < n)
         res[i++] = this.submember.readelem(buf);
   } else {
      while (i < n)
         res[i++] = buf.classStreamer({}, this.conttype);
   }

   return res;
}

/** @summary Create streamer info for pair object
  * @private */
createPairStreamer = function(typename, file) {
   let si = file.findStreamerInfo(typename);
   if (si)
      return si;
   let p1 = typename.indexOf('<');
   const p2 = typename.lastIndexOf('>');
   function getNextName() {
      let res = '', p = p1 + 1, cnt = 0;
      while ((p < p2) && (cnt >= 0)) {
         switch (typename[p]) {
            case '<':
               cnt++;
               break;
            case ',':
               if (cnt === 0)
                  cnt--;
               break;
            case '>':
               cnt--;
               break;
         }
         if (cnt >= 0)
            res += typename[p];
         p++;
      }
      p1 = p - 1;
      return res.trim();
   }
   si = { _typename: 'TStreamerInfo', fClassVersion: 0, fName: typename, fElements: create(clTList) };
   si.fElements.Add(createStreamerElement('first', getNextName(), file));
   si.fElements.Add(createStreamerElement('second', getNextName(), file));
   file.fStreamerInfos.arr.push(si);
   return si;
};

/** @summary Function creates streamer for std::pair object
  * @private */
function getPairStreamer(si, typname, file) {
   if (!si)
      si = createPairStreamer(typname, file);

   const streamer = file.getStreamer(typname, null, si);
   if (!streamer)
      return null;

   if (streamer.length !== 2) {
      console.error(`Streamer for pair class contains ${streamer.length} elements`);
      return null;
   }

   for (let nn = 0; nn < 2; ++nn) {
      if (streamer[nn].readelem && !streamer[nn].pair_name) {
         streamer[nn].pair_name = (nn === 0) ? 'first' : 'second';
         streamer[nn].func = function(buf, obj) {
            obj[this.pair_name] = this.readelem(buf);
         };
      }
   }

   return streamer;
}

/** @summary Function used in streamer to read std::map object
  * @private */
function readMapElement(buf) {
   let streamer = this.streamer;

   if (this.member_wise) {
      // when member-wise streaming is used, version is written
      const si = buf.fFile.findStreamerInfo(this.pairtype, this.stl_version.val, this.stl_version.checksum);

      if (si && (this.si !== si)) {
         streamer = getPairStreamer(si, this.pairtype, buf.fFile);
         if (streamer?.length !== 2) {
            console.log(`Fail to produce streamer for ${this.pairtype}`);
            return null;
         }
      }
   }

   const n = buf.ntoi4(), res = new Array(n);

   // no extra data written for empty map
   if (n === 0)
      return res;

   if (this.member_wise && (buf.remain() >= 6)) {
      if (buf.ntoi2() === kStreamedMemberWise)
         buf.shift(4); // skip checksum
      else
         buf.shift(-2); // rewind
   }

   for (let i = 0; i < n; ++i) {
      res[i] = { _typename: this.pairtype };
      streamer[0].func(buf, res[i]);
      if (!this.member_wise)
         streamer[1].func(buf, res[i]);
   }

   // due-to member-wise streaming second element read after first is completed
   if (this.member_wise) {
      if (buf.remain() >= 6) {
         if (buf.ntoi2() === kStreamedMemberWise)
            buf.shift(4);  // skip checksum
         else
            buf.shift(-2);  // rewind
      }
      for (let i = 0; i < n; ++i)
         streamer[1].func(buf, res[i]);
   }

   return res;
}


/** @summary create member entry for streamer element
  * @desc used for reading of data
  * @private */
function createMemberStreamer(element, file) {
   const member = {
      name: element.fName, type: element.fType,
      fArrayLength: element.fArrayLength,
      fArrayDim: element.fArrayDim,
      fMaxIndex: element.fMaxIndex
   };

   if (element.fTypeName === kBaseClass) {
      if (getArrayKind(member.name) > 0) {
         // this is workaround for arrays as base class
         // we create 'fArray' member, which read as any other data member
         member.name = 'fArray';
         member.type = kAny;
      } else {
         // create streamer for base class
         member.type = kBase;
         // this.getStreamer(element.fName);
      }
   }

   switch (member.type) {
      case kBase:
         member.base = element.fBaseVersion; // indicate base class
         member.basename = element.fName; // keep class name
         member.func = function(buf, obj) { buf.classStreamer(obj, this.basename); };
         break;
      case kShort:
         member.func = function(buf, obj) { obj[this.name] = buf.ntoi2(); };
         break;
      case kInt:
      case kCounter:
         member.func = function(buf, obj) { obj[this.name] = buf.ntoi4(); };
         break;
      case kLong:
      case kLong64:
         member.func = function(buf, obj) { obj[this.name] = buf.ntoi8(); };
         break;
      case kDouble:
         member.func = function(buf, obj) { obj[this.name] = buf.ntod(); };
         break;
      case kFloat:
         member.func = function(buf, obj) { obj[this.name] = buf.ntof(); };
         break;
      case kLegacyChar:
      case kUChar:
         member.func = function(buf, obj) { obj[this.name] = buf.ntou1(); };
         break;
      case kUShort:
         member.func = function(buf, obj) { obj[this.name] = buf.ntou2(); };
         break;
      case kBits:
      case kUInt:
         member.func = function(buf, obj) { obj[this.name] = buf.ntou4(); };
         break;
      case kULong64:
      case kULong:
         member.func = function(buf, obj) { obj[this.name] = buf.ntou8(); };
         break;
      case kBool:
         member.func = function(buf, obj) { obj[this.name] = buf.ntobool(); };
         break;
      case kOffsetL + kBool:
      case kOffsetL + kInt:
      case kOffsetL + kCounter:
      case kOffsetL + kDouble:
      case kOffsetL + kUChar:
      case kOffsetL + kShort:
      case kOffsetL + kUShort:
      case kOffsetL + kBits:
      case kOffsetL + kUInt:
      case kOffsetL + kULong:
      case kOffsetL + kULong64:
      case kOffsetL + kLong:
      case kOffsetL + kLong64:
      case kOffsetL + kFloat:
         if (element.fArrayDim < 2) {
            member.arrlength = element.fArrayLength;
            member.func = function(buf, obj) {
               obj[this.name] = buf.readFastArray(this.arrlength, this.type - kOffsetL);
            };
         } else {
            member.arrlength = element.fMaxIndex[element.fArrayDim - 1];
            member.minus1 = true;
            member.func = function(buf, obj) {
               obj[this.name] = buf.readNdimArray(this, (buf2, handle) =>
                  buf2.readFastArray(handle.arrlength, handle.type - kOffsetL));
            };
         }
         break;
      case kOffsetL + kChar:
         if (element.fArrayDim < 2) {
            member.arrlength = element.fArrayLength;
            member.func = function(buf, obj) {
               obj[this.name] = buf.readFastString(this.arrlength);
            };
         } else {
            member.minus1 = true; // one dimension used for char*
            member.arrlength = element.fMaxIndex[element.fArrayDim - 1];
            member.func = function(buf, obj) {
               obj[this.name] = buf.readNdimArray(this, (buf2, handle) =>
                  buf2.readFastString(handle.arrlength));
            };
         }
         break;
      case kOffsetP + kBool:
      case kOffsetP + kInt:
      case kOffsetP + kDouble:
      case kOffsetP + kUChar:
      case kOffsetP + kShort:
      case kOffsetP + kUShort:
      case kOffsetP + kBits:
      case kOffsetP + kUInt:
      case kOffsetP + kULong:
      case kOffsetP + kULong64:
      case kOffsetP + kLong:
      case kOffsetP + kLong64:
      case kOffsetP + kFloat:
         member.cntname = element.fCountName;
         member.func = function(buf, obj) {
            obj[this.name] = (buf.ntou1() === 1) ? buf.readFastArray(obj[this.cntname], this.type - kOffsetP) : [];
         };
         break;
      case kOffsetP + kChar:
         member.cntname = element.fCountName;
         member.func = function(buf, obj) {
            obj[this.name] = (buf.ntou1() === 1) ? buf.readFastString(obj[this.cntname]) : null;
         };
         break;
      case kDouble32:
      case kOffsetL + kDouble32:
      case kOffsetP + kDouble32:
         member.double32 = true;
      // eslint-disable-next-line  no-fallthrough
      case kFloat16:
      case kOffsetL + kFloat16:
      case kOffsetP + kFloat16:
         if (element.fFactor) {
            member.factor = 1 / element.fFactor;
            member.min = element.fXmin;
            member.read = function(buf) { return buf.ntou4() * this.factor + this.min; };
         } else
            if ((element.fXmin === 0) && member.double32)
               member.read = function(buf) { return buf.ntof(); };
            else {
               member.nbits = Math.round(element.fXmin);
               if (member.nbits === 0)
                  member.nbits = 12;
               member.dv = new DataView(new ArrayBuffer(8), 0); // used to cast from uint32 to float32
               member.read = function(buf) {
                  const theExp = buf.ntou1(), theMan = buf.ntou2();
                  this.dv.setUint32(0, (theExp << 23) | ((theMan & ((1 << (this.nbits + 1)) - 1)) << (23 - this.nbits)));
                  return ((1 << (this.nbits + 1) & theMan) ? -1 : 1) * this.dv.getFloat32(0);
               };
            }

         member.readarr = function(buf, len) {
            const arr = this.double32 ? new Float64Array(len) : new Float32Array(len);
            for (let n = 0; n < len; ++n)
               arr[n] = this.read(buf);
            return arr;
         };

         if (member.type < kOffsetL)
            member.func = function(buf, obj) { obj[this.name] = this.read(buf); };
         else
            if (member.type > kOffsetP) {
               member.cntname = element.fCountName;
               member.func = function(buf, obj) {
                  obj[this.name] = (buf.ntou1() === 1) ? this.readarr(buf, obj[this.cntname]) : null;
               };
            } else
               if (element.fArrayDim < 2) {
                  member.arrlength = element.fArrayLength;
                  member.func = function(buf, obj) { obj[this.name] = this.readarr(buf, this.arrlength); };
               } else {
                  member.arrlength = element.fMaxIndex[element.fArrayDim - 1];
                  member.minus1 = true;
                  member.func = function(buf, obj) {
                     obj[this.name] = buf.readNdimArray(this, (buf2, handle) => handle.readarr(buf2, handle.arrlength));
                  };
               }
         break;

      case kAnyP:
      case kObjectP:
         member.func = function(buf, obj) {
            obj[this.name] = buf.readNdimArray(this, buf2 => buf2.readObjectAny());
         };
         break;

      case kAny:
      case kAnyp:
      case kObjectp:
      case kObject: {
         let classname = (element.fTypeName === kBaseClass) ? element.fName : element.fTypeName;
         if (classname.at(-1) === '*')
            classname = classname.slice(0, classname.length - 1);

         const arrkind = getArrayKind(classname);

         if (arrkind > 0) {
            member.arrkind = arrkind;
            member.func = function(buf, obj) { obj[this.name] = buf.readFastArray(buf.ntou4(), this.arrkind); };
         } else if (arrkind === 0)
            member.func = function(buf, obj) { obj[this.name] = buf.readTString(); };
         else {
            member.classname = classname;

            if (element.fArrayLength > 1) {
               member.func = function(buf, obj) {
                  obj[this.name] = buf.readNdimArray(this, (buf2, handle) => buf2.classStreamer({}, handle.classname));
               };
            } else {
               member.func = function(buf, obj) {
                  obj[this.name] = buf.classStreamer({}, this.classname);
               };
            }
         }
         break;
      }
      case kOffsetL + kObject:
      case kOffsetL + kAny:
      case kOffsetL + kAnyp:
      case kOffsetL + kObjectp: {
         let classname = element.fTypeName;
         if (classname.at(-1) === '*')
            classname = classname.slice(0, classname.length - 1);

         member.arrkind = getArrayKind(classname);
         if (member.arrkind < 0)
            member.classname = classname;
         member.func = function(buf, obj) {
            obj[this.name] = buf.readNdimArray(this, (buf2, handle) => {
               if (handle.arrkind > 0)
                  return buf2.readFastArray(buf.ntou4(), handle.arrkind);
               if (handle.arrkind === 0)
                  return buf2.readTString();
               return buf2.classStreamer({}, handle.classname);
            });
         };
         break;
      }
      case kChar:
         member.func = function(buf, obj) { obj[this.name] = buf.ntoi1(); };
         break;
      case kCharStar:
         member.func = function(buf, obj) {
            const len = buf.ntoi4();
            obj[this.name] = buf.substring(buf.o, buf.o + len);
            buf.o += len;
         };
         break;
      case kTString:
         member.func = function(buf, obj) { obj[this.name] = buf.readTString(); };
         break;
      case kTObject:
      case kTNamed:
         member.typename = element.fTypeName;
         member.func = function(buf, obj) { obj[this.name] = buf.classStreamer({}, this.typename); };
         break;
      case kOffsetL + kTString:
      case kOffsetL + kTObject:
      case kOffsetL + kTNamed:
         member.typename = element.fTypeName;
         member.func = function(buf, obj) {
            const ver = buf.readVersion();
            obj[this.name] = buf.readNdimArray(this, (buf2, handle) => {
               if (handle.typename === clTString)
                  return buf2.readTString();
               return buf2.classStreamer({}, handle.typename);
            });
            buf.checkByteCount(ver, this.typename + '[]');
         };
         break;
      case kStreamLoop:
      case kOffsetL + kStreamLoop:
         member.typename = element.fTypeName;
         member.cntname = element.fCountName;

         if (member.typename.lastIndexOf('**') > 0) {
            member.typename = member.typename.slice(0, member.typename.lastIndexOf('**'));
            member.isptrptr = true;
         } else {
            member.typename = member.typename.slice(0, member.typename.lastIndexOf('*'));
            member.isptrptr = false;
         }

         if (member.isptrptr)
            member.readitem = function(buf) { return buf.readObjectAny(); };
         else {
            member.arrkind = getArrayKind(member.typename);
            if (member.arrkind > 0)
               member.readitem = function(buf) { return buf.readFastArray(buf.ntou4(), this.arrkind); };
            else if (member.arrkind === 0)
               member.readitem = function(buf) { return buf.readTString(); };
            else
               member.readitem = function(buf) { return buf.classStreamer({}, this.typename); };
         }

         if (member.readitem !== undefined) {
            member.read_loop = function(buf, cnt) {
               return buf.readNdimArray(this, (buf2, member2) => {
                  const itemarr = new Array(cnt);
                  for (let i = 0; i < cnt; ++i)
                     itemarr[i] = member2.readitem(buf2);
                  return itemarr;
               });
            };

            member.func = function(buf, obj) {
               const ver = buf.readVersion(),
                     res = this.read_loop(buf, obj[this.cntname]);
               obj[this.name] = buf.checkByteCount(ver, this.typename) ? res : null;
            };
            member.branch_func = function(buf, obj) {
               // this is special functions, used by branch in the STL container
               const ver = buf.readVersion(), sz0 = obj[this.stl_size], res = new Array(sz0);

               for (let loop0 = 0; loop0 < sz0; ++loop0) {
                  const cnt = obj[this.cntname][loop0];
                  res[loop0] = this.read_loop(buf, cnt);
               }
               obj[this.name] = buf.checkByteCount(ver, this.typename) ? res : null;
            };

            member.objs_branch_func = function(buf, obj) {
               // special function when branch read as part of complete object
               // objects already preallocated and only appropriate member must be set
               // see code in JSRoot.tree.js for reference

               const ver = buf.readVersion(),
                     arr = obj[this.name0]; // objects array where reading is done

               for (let loop0 = 0; loop0 < arr.length; ++loop0) {
                  const obj1 = this.get(arr, loop0), cnt = obj1[this.cntname];
                  obj1[this.name] = this.read_loop(buf, cnt);
               }

               buf.checkByteCount(ver, this.typename);
            };
         } else {
            console.error(`fail to provide function for ${element.fName} (${element.fTypeName})  typ = ${element.fType}`);
            member.func = function(buf, obj) {
               const ver = buf.readVersion();
               buf.checkByteCount(ver);
               obj[this.name] = null;
            };
         }

         break;

      case kStreamer: {
         member.typename = element.fTypeName;

         const stl = (element.fSTLtype || 0) % 40;
         if ((element._typename === 'TStreamerSTLstring') ||
            (member.typename === 'string') || (member.typename === 'string*'))
            member.readelem = buf => buf.readTString();
         else if ((stl === kSTLvector) || (stl === kSTLlist) ||
                    (stl === kSTLdeque) || (stl === kSTLset) || (stl === kSTLmultiset)) {
            const p1 = member.typename.indexOf('<'),
                  p2 = member.typename.lastIndexOf('>');

            member.conttype = member.typename.slice(p1 + 1, p2).trim();
            member.typeid = getTypeId(member.conttype);
            if ((member.typeid < 0) && file.fBasicTypes[member.conttype]) {
               member.typeid = file.fBasicTypes[member.conttype];
               console.log(`Reuse basic type ${member.conttype} from file streamer infos`);
            }

            // check
            if (element.fCtype && (element.fCtype < 20) && (element.fCtype !== member.typeid)) {
               console.warn(`Contained type ${member.conttype} not recognized as basic type ${element.fCtype} FORCE`);
               member.typeid = element.fCtype;
            }

            if (member.typeid > 0) {
               member.readelem = function(buf) {
                  return buf.readFastArray(buf.ntoi4(), this.typeid);
               };
            } else {
               member.isptr = false;

               if (member.conttype.at(-1) === '*') {
                  member.isptr = true;
                  member.conttype = member.conttype.slice(0, member.conttype.length - 1);
               }

               if (element.fCtype === kObjectp)
                  member.isptr = true;

               member.arrkind = getArrayKind(member.conttype);

               member.readelem = readVectorElement;

               if (!member.isptr && (member.arrkind < 0)) {
                  const subelem = createStreamerElement('temp', member.conttype, file);
                  if (subelem.fType === kStreamer) {
                     subelem.$fictional = true;
                     member.submember = createMemberStreamer(subelem, file);
                  }
               }
            }
         } else if ((stl === kSTLmap) || (stl === kSTLmultimap)) {
            const p1 = member.typename.indexOf('<'),
                  p2 = member.typename.lastIndexOf('>');

            member.pairtype = 'pair<' + member.typename.slice(p1 + 1, p2) + '>';

            // remember found streamer info from the file -
            // most probably it is the only one which should be used
            member.si = file.findStreamerInfo(member.pairtype);

            member.streamer = getPairStreamer(member.si, member.pairtype, file);

            if (!member.streamer || (member.streamer.length !== 2)) {
               console.error(`Fail to build streamer for pair ${member.pairtype}`);
               delete member.streamer;
            }

            if (member.streamer)
               member.readelem = readMapElement;
         } else if (stl === kSTLbitset)
            member.readelem = (buf /* , obj */) => buf.readFastArray(buf.ntou4(), kBool);

         if (!member.readelem) {
            console.error(`failed to create streamer for element ${member.typename} ${member.name} element ${element._typename} STL type ${element.fSTLtype}`);
            member.func = function(buf, obj) {
               const ver = buf.readVersion();
               buf.checkByteCount(ver);
               obj[this.name] = null;
            };
         } else if (!element.$fictional) {
            member.read_version = function(buf, cnt) {
               if (cnt === 0)
                  return null;
               const ver = buf.readVersion();
               this.member_wise = Boolean(ver.val & kStreamedMemberWise);

               this.stl_version = undefined;
               if (this.member_wise) {
                  ver.val &= ~kStreamedMemberWise;
                  this.stl_version = { val: buf.ntoi2() };
                  if (this.stl_version.val <= 0)
                     this.stl_version.checksum = buf.ntou4();
               }
               return ver;
            };
            member.func = function(buf, obj) {
               const ver = this.read_version(buf),
                     res = buf.readNdimArray(this, (buf2, member2) => member2.readelem(buf2));
               obj[this.name] = buf.checkByteCount(ver, this.typename) ? res : null;
            };
            member.branch_func = function(buf, obj) {
               // special function to read data from STL branch
               const cnt = obj[this.stl_size],
                     ver = this.read_version(buf, cnt),
                     arr = new Array(cnt);

               for (let n = 0; n < cnt; ++n)
                  arr[n] = buf.readNdimArray(this, (buf2, member2) => member2.readelem(buf2));

               if (ver)
                  buf.checkByteCount(ver, `branch ${this.typename}`);

               obj[this.name] = arr;
            };
            member.split_func = function(buf, arr, n) {
               // function to read array from member-wise streaming
               const ver = this.read_version(buf);
               for (let i = 0; i < n; ++i)
                  arr[i][this.name] = buf.readNdimArray(this, (buf2, member2) => member2.readelem(buf2));
               buf.checkByteCount(ver, this.typename);
            };
            member.objs_branch_func = function(buf, obj) {
               // special function when branch read as part of complete object
               // objects already preallocated and only appropriate member must be set
               // see code in JSRoot.tree.js for reference

               const arr = obj[this.name0], // objects array where reading is done
                     ver = this.read_version(buf, arr.length);

               for (let n = 0; n < arr.length; ++n) {
                  const obj1 = this.get(arr, n);
                  obj1[this.name] = buf.readNdimArray(this, (buf2, member2) => member2.readelem(buf2));
               }

               if (ver)
                  buf.checkByteCount(ver, `branch ${this.typename}`);
            };
         }
         break;
      }

      default:
         console.error(`fail to provide function for ${element.fName} (${element.fTypeName})  typ = ${element.fType}`);

         member.func = function(/* buf, obj */) {};  // do nothing, fix in the future
   }

   return member;
}


/** @summary Let directly assign methods when doing I/O
  * @private */
function addClassMethods(clname, streamer) {
   if (streamer === null)
      return streamer;

   const methods = getMethods(clname);

   if (methods) {
      for (const key in methods) {
         if (isFunc(methods[key]) || (key.indexOf('_') === 0))
            streamer.push({ name: key, method: methods[key], func(_buf, obj) { obj[this.name] = this.method; } });
      }
   }

   return streamer;
}


/* Copyright (C) 1999 Masanao Izumo <iz@onicos.co.jp>
 * Version: 1.0.0.1
 * LastModified: Dec 25 1999
 * original: http://www.onicos.com/staff/iz/amuse/javascript/expert/inflate.txt
 */

/* constant parameters */
const
   zip_WSIZE = 32768,       // Sliding Window size

   /* constant tables (inflate) */
   zip_MASK_BITS = [0x0000, 0x0001, 0x0003, 0x0007, 0x000f, 0x001f, 0x003f, 0x007f, 0x00ff,
                    0x01ff, 0x03ff, 0x07ff, 0x0fff, 0x1fff, 0x3fff, 0x7fff, 0xffff],

   // Tables for deflate from PKZIP's appnote.txt.
   // Copy lengths for literal codes 257..285
   zip_cplens = [3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31,
                 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0],

   /* note: see note #13 above about the 258 in this list. */
   // Extra bits for literal codes 257..285
   zip_cplext = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2,
                 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0, 99, 99], // 99==invalid

   // Copy offsets for distance codes 0..29
   zip_cpdist = [1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193,
                 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145,
                 8193, 12289, 16385, 24577],

   // Extra bits for distance codes
   zip_cpdext = [0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6,
                 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13],

   // Order of the bit length code lengths
   zip_border = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15];

function ZIP_inflate(arr, tgt) {
   /* variables (inflate) */
   const zip_slide = new Array(2 * zip_WSIZE),
         zip_inflate_data = arr,
         zip_inflate_datalen = arr.byteLength;
   let zip_wp = 0,                // current position in slide
       zip_fixed_tl = null,      // inflate static
       zip_fixed_td,             // inflate static
       zip_fixed_bl, zip_fixed_bd,   // inflate static
       zip_bit_buf = 0,            // bit buffer
       zip_bit_len = 0,           // bits in bit buffer
       zip_method = -1,
       zip_eof = false,
       zip_copy_leng = 0,
       zip_copy_dist = 0,
       zip_tl = null, zip_td,    // literal/length and distance decoder tables
       zip_bl, zip_bd,           // number of bits decoded by tl and td
       zip_inflate_pos = 0;

   function zip_NEEDBITS(n) {
      while (zip_bit_len < n) {
         if (zip_inflate_pos < zip_inflate_datalen)
            zip_bit_buf |= zip_inflate_data[zip_inflate_pos++] << zip_bit_len;
         zip_bit_len += 8;
      }
   }

   function zip_GETBITS(n) {
      return zip_bit_buf & zip_MASK_BITS[n];
   }

   function zip_DUMPBITS(n) {
      zip_bit_buf >>= n;
      zip_bit_len -= n;
   }

   /* objects (inflate) */
   function zip_HuftBuild(b,     // code lengths in bits (all assumed <= BMAX)
                          n,     // number of codes (assumed <= N_MAX)
                          s,     // number of simple-valued codes (0..s-1)
                          d,     // list of base values for non-simple codes
                          e,     // list of extra bits for non-simple codes
                          mm) {  // maximum lookup bits
      const BMAX = 16,      // maximum bit length of any code
            N_MAX = 288,    // maximum number of codes in any set
            c = Array(BMAX + 1).fill(0),  // bit length count table
            lx = Array(BMAX + 1).fill(0), // stack of bits per table
            u = Array(BMAX).fill(null), // zip_HuftNode[BMAX][]  table stack
            v = Array(N_MAX).fill(0), // values in order of bit length
            x = Array(BMAX + 1).fill(0), // bit offsets, then code stack
            r = { e: 0, b: 0, n: 0, t: null }, // new zip_HuftNode(), // table entry for structure assignment
            el = (n > 256) ? b[256] : BMAX, // set length of EOB code, if any
            res = {
               status: 0,    // 0: success, 1: incomplete table, 2: bad input
               root: null,   // (zip_HuftList) starting table
               m: 0          // maximum lookup bits, returns actual
            };
      let rr,        // temporary variable, use in assignment
          a,         // counter for codes of length k
          f,         // i repeats in table every f entries
          h,         // table level
          j,         // counter
          k,         // number of bits in current code
          p = b,     // pointer into c[], b[], or v[]
          pidx = 0,  // index of p
          q,         // (zip_HuftNode) points to current table
          w,
          xp,        // pointer into x or c
          y,         // number of dummy codes added
          z,         // number of entries in current table
          o,
          tail = null,   // (zip_HuftList)
          i = n;         // counter, current code

      // Generate counts for each bit length
      do
         c[p[pidx++]]++; // assume all entries <= BMAX
      while (--i > 0);

      if (c[0] === n)    // null input--all zero length codes
         return res;

      // Find minimum and maximum length, bound *m by those
      for (j = 1; j <= BMAX && !c[j]; ++j);

      k = j;         // minimum code length
      if (mm < j)
         mm = j;
      for (i = BMAX; i && !c[i]; --i);

      const g = i;         // maximum code length
      if (mm > i)
         mm = i;

      // Adjust last length count to fill out codes, if needed
      for (y = 1 << j; j < i; ++j, y <<= 1) {
         if ((y -= c[j]) < 0) {
            res.status = 2;  // bad input: more codes than bits
            res.m = mm;
            return res;
         }
      }
      if ((y -= c[i]) < 0) {
         res.status = 2;
         res.m = mm;
         return res;
      }
      c[i] += y;

      // Generate starting offsets into the value table for each length
      x[1] = j = 0;
      p = c;
      pidx = 1;
      xp = 2;
      while (--i > 0)    // note that i == g from above
         x[xp++] = (j += p[pidx++]);

      // Make a table of values in order of bit lengths
      p = b;
      pidx = 0;
      i = 0;
      do {
         if ((j = p[pidx++]))
            v[x[j]++] = i;
      } while (++i < n);
      n = x[g];         // set n to length of v

      // Generate the Huffman codes and for each, make the table entries
      x[0] = i = 0;     // first Huffman code is zero
      p = v;
      pidx = 0;     // grab values in bit order
      h = -1;        // no tables yet--level -1
      w = lx[0] = 0;    // no bits decoded yet
      q = null;         // ditto
      z = 0;         // ditto

      // go through the bit lengths (k already is bits in shortest code)
      for (; k <= g; ++k) {
         a = c[k];
         while (a-- > 0) {
            // here i is the Huffman code of length k bits for value p[pidx]
            // make tables up to required level
            while (k > w + lx[1 + h]) {
               w += lx[1 + h++]; // add bits already decoded

               // compute minimum size table less than or equal to *m bits
               z = (z = g - w) > mm ? mm : z; // upper limit
               if ((f = 1 << (j = k - w)) > a + 1) { // try a k-w bit table
                  // too few codes for k-w bit table
                  f -= a + 1; // deduct codes from patterns left
                  xp = k;
                  while (++j < z) { // try smaller tables up to z bits
                     if ((f <<= 1) <= c[++xp])
                        break;   // enough codes to use up j bits
                     f -= c[xp];   // else deduct codes from patterns
                  }
               }
               if (w + j > el && w < el)
                  j = el - w; // make EOB code end at table
               z = 1 << j;   // table entries for j-bit table
               lx[1 + h] = j; // set table size in stack

               // allocate and link in new table
               q = new Array(z);
               for (o = 0; o < z; ++o)
                  q[o] = { e: 0, b: 0, n: 0, t: null }; // new zip_HuftNode

               if (tail === null)
                  tail = res.root = { next: null, list: null }; // new zip_HuftList();
               else
                  tail = tail.next = { next: null, list: null }; // new zip_HuftList();
               tail.next = null;
               tail.list = q;
               u[h] = q;  // table starts after link

               /* connect to last table, if there is one */
               if (h > 0) {
                  x[h] = i;      // save pattern for backing up
                  r.b = lx[h];   // bits to dump before this table
                  r.e = 16 + j;  // bits in this table
                  r.t = q;    // pointer to this table
                  j = (i & ((1 << w) - 1)) >> (w - lx[h]);
                  rr = u[h - 1][j];
                  rr.e = r.e;
                  rr.b = r.b;
                  rr.n = r.n;
                  rr.t = r.t;
               }
            }

            // set up table entry in r
            r.b = k - w;
            if (pidx >= n)
               r.e = 99;     // out of values--invalid code
            else if (p[pidx] < s) {
               r.e = (p[pidx] < 256 ? 16 : 15); // 256 is end-of-block code
               r.n = p[pidx++]; // simple code is just the value
            } else {
               r.e = e[p[pidx] - s];  // non-simple--look up in lists
               r.n = d[p[pidx++] - s];
            }

            // fill code-like entries with r //
            f = 1 << (k - w);
            for (j = i >> w; j < z; j += f) {
               rr = q[j];
               rr.e = r.e;
               rr.b = r.b;
               rr.n = r.n;
               rr.t = r.t;
            }

            // backwards increment the k-bit code i
            for (j = 1 << (k - 1); (i & j); j >>= 1)
               i ^= j;
            i ^= j;

            // backup over finished tables
            while ((i & ((1 << w) - 1)) !== x[h])
               w -= lx[h--];      // don't need to update q
         }
      }

      /* return actual size of base table */
      res.m = lx[1];

      /* Return true (1) if we were given an incomplete table */
      res.status = (y && g !== 1) ? 1 : 0;

      return res;
   }

   /* routines (inflate) */

   function zip_inflate_codes(buff, off, size) {
      if (size === 0)
         return 0;

      /* inflate (decompress) the codes in a deflated (compressed) block.
         Return an error code or zero if it all goes ok. */

      let e,     // table entry flag/number of extra bits
          t,     // (zip_HuftNode) pointer to table entry
          n = 0;

      // inflate the coded data
      for (;;) {        // do until end of block
         zip_NEEDBITS(zip_bl);
         t = zip_tl.list[zip_GETBITS(zip_bl)];
         e = t.e;
         while (e > 16) {
            if (e === 99)
               return -1;
            zip_DUMPBITS(t.b);
            e -= 16;
            zip_NEEDBITS(e);
            t = t.t[zip_GETBITS(e)];
            e = t.e;
         }
         zip_DUMPBITS(t.b);

         if (e === 16) {     // then it's a literal
            zip_wp &= zip_WSIZE - 1;
            buff[off + n++] = zip_slide[zip_wp++] = t.n;
            if (n === size)
               return size;
            continue;
         }

         // exit if end of block
         if (e === 15)
            break;

         // it's an EOB or a length

         // get length of block to copy
         zip_NEEDBITS(e);
         zip_copy_leng = t.n + zip_GETBITS(e);
         zip_DUMPBITS(e);

         // decode distance of block to copy
         zip_NEEDBITS(zip_bd);
         t = zip_td.list[zip_GETBITS(zip_bd)];
         e = t.e;

         while (e > 16) {
            if (e === 99)
               return -1;
            zip_DUMPBITS(t.b);
            e -= 16;
            zip_NEEDBITS(e);
            t = t.t[zip_GETBITS(e)];
            e = t.e;
         }
         zip_DUMPBITS(t.b);
         zip_NEEDBITS(e);
         zip_copy_dist = zip_wp - t.n - zip_GETBITS(e);
         zip_DUMPBITS(e);

         // do the copy
         while (zip_copy_leng > 0 && n < size) {
            --zip_copy_leng;
            zip_copy_dist &= zip_WSIZE - 1;
            zip_wp &= zip_WSIZE - 1;
            buff[off + n++] = zip_slide[zip_wp++] = zip_slide[zip_copy_dist++];
         }

         if (n === size)
            return size;
      }

      zip_method = -1; // done
      return n;
   }

   function zip_inflate_stored(buff, off, size) {
      /* 'decompress' an inflated type 0 (stored) block. */

      // go to byte boundary
      let n = zip_bit_len & 7;
      zip_DUMPBITS(n);

      // get the length and its complement
      zip_NEEDBITS(16);
      n = zip_GETBITS(16);
      zip_DUMPBITS(16);
      zip_NEEDBITS(16);
      if (n !== ((~zip_bit_buf) & 0xffff))
         return -1;        // error in compressed data
      zip_DUMPBITS(16);

      // read and output the compressed data
      zip_copy_leng = n;

      n = 0;
      while (zip_copy_leng > 0 && n < size) {
         --zip_copy_leng;
         zip_wp &= zip_WSIZE - 1;
         zip_NEEDBITS(8);
         buff[off + n++] = zip_slide[zip_wp++] = zip_GETBITS(8);
         zip_DUMPBITS(8);
      }

      if (zip_copy_leng === 0)
         zip_method = -1; // done
      return n;
   }

   function zip_inflate_fixed(buff, off, size) {
      /* decompress an inflated type 1 (fixed Huffman codes) block.  We should
         either replace this with a custom decoder, or at least pre-compute the
         Huffman tables. */

      // if first time, set up tables for fixed blocks
      if (zip_fixed_tl === null) {
         // literal table
         const l = Array(288).fill(8, 0, 144).fill(9, 144, 256).fill(7, 256, 280).fill(8, 280, 288);
         // make a complete, but wrong code set
         zip_fixed_bl = 7;

         let h = zip_HuftBuild(l, 288, 257, zip_cplens, zip_cplext, zip_fixed_bl);
         if (h.status)
            throw new Error('HufBuild error: ' + h.status);
         zip_fixed_tl = h.root;
         zip_fixed_bl = h.m;

         // distance table
         l.fill(5, 0, 30); // make an incomplete code set
         zip_fixed_bd = 5;

         h = zip_HuftBuild(l, 30, 0, zip_cpdist, zip_cpdext, zip_fixed_bd);
         if (h.status > 1) {
            zip_fixed_tl = null;
            throw new Error('HufBuild error: ' + h.status);
         }
         zip_fixed_td = h.root;
         zip_fixed_bd = h.m;
      }

      zip_tl = zip_fixed_tl;
      zip_td = zip_fixed_td;
      zip_bl = zip_fixed_bl;
      zip_bd = zip_fixed_bd;
      return zip_inflate_codes(buff, off, size);
   }

   function zip_inflate_dynamic(buff, off, size) {
      // decompress an inflated type 2 (dynamic Huffman codes) block.
      let i, j,  // temporary variables
          l,     // last length
          t,     // (zip_HuftNode) literal/length code table
          h;     // (zip_HuftBuild)
      const ll = new Array(286 + 30).fill(0); // literal/length and distance code lengths

      // read in table lengths
      zip_NEEDBITS(5);
      const nl = 257 + zip_GETBITS(5);   // number of literal/length codes
      zip_DUMPBITS(5);
      zip_NEEDBITS(5);
      const nd = 1 + zip_GETBITS(5);  // number of distance codes
      zip_DUMPBITS(5);
      zip_NEEDBITS(4);
      const nb = 4 + zip_GETBITS(4);  // number of bit length codes
      zip_DUMPBITS(4);
      if (nl > 286 || nd > 30)
         return -1;     // bad lengths

      // read in bit-length-code lengths
      for (j = 0; j < nb; ++j) {
         zip_NEEDBITS(3);
         ll[zip_border[j]] = zip_GETBITS(3);
         zip_DUMPBITS(3);
      }
      for (; j < 19; ++j)
         ll[zip_border[j]] = 0;

      // build decoding table for trees--single level, 7 bit lookup
      zip_bl = 7;
      h = zip_HuftBuild(ll, 19, 19, null, null, zip_bl);
      if (h.status)
         return -1;  // incomplete code set

      zip_tl = h.root;
      zip_bl = h.m;

      // read in literal and distance code lengths
      const n = nl + nd;   // number of lengths to get
      i = l = 0;
      while (i < n) {
         zip_NEEDBITS(zip_bl);
         t = zip_tl.list[zip_GETBITS(zip_bl)];
         j = t.b;
         zip_DUMPBITS(j);
         j = t.n;
         if (j < 16) // length of code in bits (0..15)
            ll[i++] = l = j; // save last length in l
         else if (j === 16) {   // repeat last length 3 to 6 times
            zip_NEEDBITS(2);
            j = 3 + zip_GETBITS(2);
            zip_DUMPBITS(2);
            if (i + j > n)
               return -1;
            while (j-- > 0)
               ll[i++] = l;
         } else if (j === 17) { // 3 to 10 zero length codes
            zip_NEEDBITS(3);
            j = 3 + zip_GETBITS(3);
            zip_DUMPBITS(3);
            if (i + j > n)
               return -1;
            while (j-- > 0)
               ll[i++] = 0;
            l = 0;
         } else {    // j == 18: 11 to 138 zero length codes
            zip_NEEDBITS(7);
            j = 11 + zip_GETBITS(7);
            zip_DUMPBITS(7);
            if (i + j > n)
               return -1;
            while (j-- > 0)
               ll[i++] = 0;
            l = 0;
         }
      }

      // build the decoding tables for literal/length and distance codes
      zip_bl = 9; // zip_lbits;
      h = zip_HuftBuild(ll, nl, 257, zip_cplens, zip_cplext, zip_bl);
      if (zip_bl === 0)  // no literals or lengths
         h.status = 1;
      if (h.status)
         return -1;     // incomplete code set
      zip_tl = h.root;
      zip_bl = h.m;

      for (i = 0; i < nd; ++i)
         ll[i] = ll[i + nl];
      zip_bd = 6; // zip_dbits;
      h = zip_HuftBuild(ll, nd, 0, zip_cpdist, zip_cpdext, zip_bd);
      zip_td = h.root;
      zip_bd = h.m;

      // incomplete distance tree
      if ((zip_bd === 0 && nl > 257) || h.status)   // lengths but no distances
         return -1;

      // decompress until an end-of-block code
      return zip_inflate_codes(buff, off, size);
   }

   function zip_inflate_internal(buff, off, size) {
      // decompress an inflated entry
      let n = 0, i;

      while (n < size) {
         if (zip_eof && zip_method === -1)
            return n;

         if (zip_copy_leng > 0) {
            if (zip_method /* zip_STORED_BLOCK */) {
               // STATIC_TREES or DYN_TREES
               while (zip_copy_leng > 0 && n < size) {
                  --zip_copy_leng;
                  zip_copy_dist &= zip_WSIZE - 1;
                  zip_wp &= zip_WSIZE - 1;
                  buff[off + n++] = zip_slide[zip_wp++] =
                  zip_slide[zip_copy_dist++];
               }
            } else {
               while (zip_copy_leng > 0 && n < size) {
                  --zip_copy_leng;
                  zip_wp &= zip_WSIZE - 1;
                  zip_NEEDBITS(8);
                  buff[off + n++] = zip_slide[zip_wp++] = zip_GETBITS(8);
                  zip_DUMPBITS(8);
               }
               if (zip_copy_leng === 0)
                  zip_method = -1; // done
            }
            if (n === size)
               return n;
         }

         if (zip_method === -1) {
            if (zip_eof)
               break;

            // read in last block bit
            zip_NEEDBITS(1);
            if (zip_GETBITS(1))
               zip_eof = true;
            zip_DUMPBITS(1);

            // read in block type
            zip_NEEDBITS(2);
            zip_method = zip_GETBITS(2);
            zip_DUMPBITS(2);
            zip_tl = null;
            zip_copy_leng = 0;
         }

         switch (zip_method) {
            case 0: // zip_STORED_BLOCK
               i = zip_inflate_stored(buff, off + n, size - n);
               break;

            case 1: // zip_STATIC_TREES
               if (zip_tl !== null)
                  i = zip_inflate_codes(buff, off + n, size - n);
               else
                  i = zip_inflate_fixed(buff, off + n, size - n);
               break;

            case 2: // zip_DYN_TREES
               if (zip_tl !== null)
                  i = zip_inflate_codes(buff, off + n, size - n);
               else
                  i = zip_inflate_dynamic(buff, off + n, size - n);
               break;

            default: // error
               i = -1;
               break;
         }

         if (i === -1)
            return zip_eof ? 0 : -1;
         n += i;
      }
      return n;
   }

   let i, cnt = 0;
   while ((i = zip_inflate_internal(tgt, cnt, Math.min(1024, tgt.byteLength - cnt))) > 0)
      cnt += i;

   return cnt;
} // function ZIP_inflate

/**
 * https://github.com/pierrec/node-lz4/blob/master/lib/binding.js
 *
 * LZ4 based compression and decompression
 * Copyright (c) 2014 Pierre Curto
 * MIT Licensed
 */

/**
 * Decode a block. Assumptions: input contains all sequences of a
 * chunk, output is large enough to receive the decoded data.
 * If the output buffer is too small, an error will be thrown.
 * If the returned value is negative, an error occurred at the returned offset.
 *
 * @param input {Buffer} input data
 * @param output {Buffer} output data
 * @return {Number} number of decoded bytes
 * @private */
function LZ4_uncompress(input, output, sIdx, eIdx) {
   sIdx = sIdx || 0;
   eIdx = eIdx || (input.length - sIdx);
   // Process each sequence in the incoming data
   let j = 0;
   for (let i = sIdx, n = eIdx; i < n;) {
      const token = input[i++];

      // Literals
      let literals_length = (token >> 4);
      if (literals_length > 0) {
         // length of literals
         let l = literals_length + 240;
         while (l === 255) {
            l = input[i++];
            literals_length += l;
         }

         // Copy the literals
         const end = i + literals_length;
         while (i < end)
            output[j++] = input[i++];

         // End of buffer?
         if (i === n)
            return j;
      }

      // Match copy
      // 2 bytes offset (little endian)
      const offset = input[i++] | (input[i++] << 8);

      // 0 is an invalid offset value
      if (offset === 0 || offset > j)
         return -(i - 2);

      // length of match copy
      let match_length = (token & 0xf),
          l = match_length + 240;
      while (l === 255) {
         l = input[i++];
         match_length += l;
      }

      // Copy the match
      let pos = j - offset; // position of the match copy in the current output
      const end = j + match_length + 4; // minmatch = 4;
      while (j < end)
         output[j++] = output[pos++];
   }

   return j;
}


/** @summary Reads header envelope, determines zipped size and unzip content
  * @return {Promise} with unzipped content
  * @private */
async function R__unzip(arr, tgtsize, noalert, src_shift) {
   const HDRSIZE = 9, totallen = arr.byteLength;

   let curr = src_shift || 0, fullres = 0, tgtbuf = null;

   const nextPortion = () => {
      while (fullres < tgtsize) {
         let fmt = 'unknown', off = 0, CHKSUM = 0;

         if (curr + HDRSIZE >= totallen) {
            if (!noalert)
               console.error('Error R__unzip: header size exceeds buffer size');
            return Promise.resolve(null);
         }

         const getCode = o => arr.getUint8(o),
               checkChar = (o, symb) => { return getCode(o) === symb.charCodeAt(0); },
               checkFmt = (a, b, c) => { return checkChar(curr, a) && checkChar(curr + 1, b) && (getCode(curr + 2) === c); };

         if (checkFmt('Z', 'L', 8)) {
            fmt = 'new';
            off = 2;
         } else if (checkFmt('C', 'S', 8))
            fmt = 'old';
         else if (checkFmt('X', 'Z', 0))
            fmt = 'LZMA';
         else if (checkFmt('Z', 'S', 1))
            fmt = 'ZSTD';
         else if (checkChar(curr, 'L') && checkChar(curr + 1, '4')) {
            fmt = 'LZ4';
            CHKSUM = 8;
         }

         /*   C H E C K   H E A D E R   */
         if ((fmt !== 'new') && (fmt !== 'old') && (fmt !== 'LZ4') && (fmt !== 'ZSTD') && (fmt !== 'LZMA')) {
            if (!noalert)
               console.error(`R__unzip: ${fmt} format is not supported!`);
            return Promise.resolve(null);
         }

         const srcsize = HDRSIZE + ((getCode(curr + 3) & 0xff) | ((getCode(curr + 4) & 0xff) << 8) | ((getCode(curr + 5) & 0xff) << 16)),
               uint8arr = new Uint8Array(arr.buffer, arr.byteOffset + curr + HDRSIZE + off + CHKSUM, Math.min(arr.byteLength - curr - HDRSIZE - off - CHKSUM, srcsize - HDRSIZE - CHKSUM));

         if (!tgtbuf)
            tgtbuf = new ArrayBuffer(tgtsize);
         const tgt8arr = new Uint8Array(tgtbuf, fullres);

         if (fmt === 'ZSTD') {
            let promise;
            if (internals._ZstdStream)
               promise = Promise.resolve(internals._ZstdStream);
            else if (internals._ZstdInit !== undefined)
               promise = new Promise(resolveFunc => { internals._ZstdInit.push(resolveFunc); });
            else {
               internals._ZstdInit = [];
               promise = (isNodeJs() ? import('@oneidentity/zstd-js') : import('./base/zstd.mjs'))
                  .then(({ ZstdInit }) => ZstdInit())
                  .then(({ ZstdStream }) => {
                     internals._ZstdStream = ZstdStream;
                     internals._ZstdInit.forEach(func => func(ZstdStream));
                     delete internals._ZstdInit;
                     return ZstdStream;
                  });
            }

            return promise.then(ZstdStream => {
               const data2 = ZstdStream.decompress(uint8arr),
                     reslen = data2.length;

               for (let i = 0; i < reslen; ++i)
                  tgt8arr[i] = data2[i];

               fullres += reslen;
               curr += srcsize;
               return nextPortion();
            });
         } else if (fmt === 'LZMA') {
            return import('./base/lzma.mjs').then(lzma => {
               const expected_len = (getCode(curr + 6) & 0xff) | ((getCode(curr + 7) & 0xff) << 8) | ((getCode(curr + 8) & 0xff) << 16),
                     reslen = lzma.decompress(uint8arr, tgt8arr, expected_len);
               fullres += reslen;
               curr += srcsize;
               return nextPortion();
            });
         }

         const reslen = (fmt === 'LZ4') ? LZ4_uncompress(uint8arr, tgt8arr) : ZIP_inflate(uint8arr, tgt8arr);

         if (reslen <= 0)
            break;
         fullres += reslen;
         curr += srcsize;
      }

      if (fullres !== tgtsize) {
         if (!noalert)
            console.error(`R__unzip: fail to unzip data expects ${tgtsize}, got ${fullres}`);
         return Promise.resolve(null);
      }

      return Promise.resolve(new DataView(tgtbuf));
   };

   return nextPortion();
}


/**
  * @summary Buffer object to read data from TFile
  *
  * @private
  */

class TBuffer {

   constructor(arr, pos, file, length) {
      this._typename = 'TBuffer';
      this.arr = arr;
      this.o = pos || 0;
      this.fFile = file;
      this.length = length || (arr ? arr.byteLength : 0); // use size of array view, blob buffer can be much bigger
      this.clearObjectMap();
      this.fTagOffset = 0;
      this.last_read_version = 0;
   }

   /** @summary locate position in the buffer  */
   locate(pos) { this.o = pos; }

   /** @summary shift position in the buffer  */
   shift(cnt) { this.o += cnt; }

   /** @summary Returns remaining place in the buffer */
   remain() { return this.length - this.o; }

   /** @summary Get mapped object with provided tag */
   getMappedObject(tag) { return this.fObjectMap[tag]; }

   /** @summary Map object */
   mapObject(tag, obj) {
      if (obj !== null)
         this.fObjectMap[tag] = obj;
   }

   /** @summary Map class */
   mapClass(tag, classname) { this.fClassMap[tag] = classname; }

   /** @summary Get mapped class with provided tag */
   getMappedClass(tag) { return (tag in this.fClassMap) ? this.fClassMap[tag] : -1; }

   /** @summary Clear objects map */
   clearObjectMap() {
      this.fObjectMap = {};
      this.fClassMap = {};
      this.fObjectMap[0] = null;
      this.fDisplacement = 0;
   }

   /** @summary  read class version from I/O buffer */
   readVersion() {
      const ver = {}, bytecnt = this.ntou4(); // byte count

      if (bytecnt & kByteCountMask)
         ver.bytecnt = bytecnt - kByteCountMask - 2; // one can check between Read version and end of streamer
      else
         this.o -= 4; // rollback read bytes, this is old buffer without byte count

      this.last_read_version = ver.val = this.ntoi2();
      this.last_read_checksum = 0;
      ver.off = this.o;

      if ((ver.val <= 0) && ver.bytecnt && (ver.bytecnt >= 4)) {
         ver.checksum = this.ntou4();
         if (!this.fFile.findStreamerInfo(undefined, undefined, ver.checksum)) {
            // console.error(`Fail to find streamer info with check sum ${ver.checksum} version ${ver.val}`);
            this.o -= 4; // not found checksum in the list
            delete ver.checksum; // remove checksum
         } else
            this.last_read_checksum = ver.checksum;
      }
      return ver;
   }

   /** @summary Check bytecount after object streaming */
   checkByteCount(ver, where) {
      if ((ver.bytecnt !== undefined) && (ver.off + ver.bytecnt !== this.o)) {
         if (where)
            console.log(`Missmatch in ${where}:${ver.val} bytecount expected = ${ver.bytecnt}  got = ${this.o - ver.off}`);
         this.o = ver.off + ver.bytecnt;
         return false;
      }
      return true;
   }

   /** @summary Read TString object (or equivalent)
     * @desc std::string uses similar binary format */
   readTString() {
      let len = this.ntou1();
      // large strings
      if (len === 255)
         len = this.ntou4();
      if (!len)
         return '';

      const pos = this.o;
      this.o += len;

      return (this.codeAt(pos) === 0) ? '' : this.substring(pos, pos + len);
   }

   /** @summary read Char_t array as string
     * @desc stops when 0 is found */
   readNullTerminatedString() {
      let res = '', code;
      while ((code = this.ntou1()))
         res += String.fromCharCode(code);
      return res;
   }

   /** @summary read Char_t array as string */
   readFastString(n) {
      let res = '', reading = true;
      for (let i = 0; i < n; ++i) {
         const code = this.ntou1();
         if (code === 0)
            reading = false;
         if (reading)
            res += String.fromCharCode(code);
      }

      return res;
   }

   /** @summary read uint8_t */
   ntou1() { return this.arr.getUint8(this.o++); }

   /** @summary read boolean */
   ntobool() { return Boolean(this.arr.getUint8(this.o++)); }

   /** @summary read uint16_t */
   ntou2() {
      const o = this.o;
      this.o += 2;
      return this.arr.getUint16(o);
   }

   /** @summary read uint32_t */
   ntou4() {
      const o = this.o;
      this.o += 4;
      return this.arr.getUint32(o);
   }

   /** @summary read uint64_t */
   ntou8() {
      const high = this.arr.getUint32(this.o);
      this.o += 4;
      const low = this.arr.getUint32(this.o);
      this.o += 4;
      return (high < 0x200000) ? (high * 0x100000000 + low) : (BigInt(high) * BigInt(0x100000000) + BigInt(low));
   }

   /** @summary read int8_t */
   ntoi1() { return this.arr.getInt8(this.o++); }

   /** @summary read int16_t */
   ntoi2() {
      const o = this.o;
      this.o += 2;
      return this.arr.getInt16(o);
   }

   /** @summary read int32_t */
   ntoi4() {
      const o = this.o;
      this.o += 4;
      return this.arr.getInt32(o);
   }

   /** @summary read int64_t */
   ntoi8() {
      const high = this.arr.getUint32(this.o);
      this.o += 4;
      const low = this.arr.getUint32(this.o);
      this.o += 4;
      if (high < 0x80000000)
         return (high < 0x200000) ? (high * 0x100000000 + low) : (BigInt(high) * BigInt(0x100000000) + BigInt(low));
      return (~high < 0x200000) ? (-1 - ((~high) * 0x100000000 + ~low)) : (BigInt(-1) - (BigInt(~high) * BigInt(0x100000000) + BigInt(~low)));
   }

   /** @summary read float */
   ntof() {
      const o = this.o;
      this.o += 4;
      return this.arr.getFloat32(o);
   }

   /** @summary read double */
   ntod() {
      const o = this.o;
      this.o += 8;
      return this.arr.getFloat64(o);
   }

   /** @summary Reads array of n values from the I/O buffer */
   readFastArray(n, array_type) {
      let array, i = 0, o = this.o;
      const view = this.arr;
      switch (array_type) {
         case kDouble:
            array = new Float64Array(n);
            for (; i < n; ++i, o += 8)
               array[i] = view.getFloat64(o);
            break;
         case kFloat:
            array = new Float32Array(n);
            for (; i < n; ++i, o += 4)
               array[i] = view.getFloat32(o);
            break;
         case kLong:
         case kLong64:
            array = new Array(n);
            for (; i < n; ++i)
               array[i] = this.ntoi8();
            return array; // exit here to avoid conflicts
         case kULong:
         case kULong64:
            array = new Array(n);
            for (; i < n; ++i)
               array[i] = this.ntou8();
            return array; // exit here to avoid conflicts
         case kInt:
         case kCounter:
            array = new Int32Array(n);
            for (; i < n; ++i, o += 4)
               array[i] = view.getInt32(o);
            break;
         case kShort:
            array = new Int16Array(n);
            for (; i < n; ++i, o += 2)
               array[i] = view.getInt16(o);
            break;
         case kUShort:
            array = new Uint16Array(n);
            for (; i < n; ++i, o += 2)
               array[i] = view.getUint16(o);
            break;
         case kChar:
            array = new Int8Array(n);
            for (; i < n; ++i)
               array[i] = view.getInt8(o++);
            break;
         case kBool:
         case kUChar:
            array = new Uint8Array(n);
            for (; i < n; ++i)
               array[i] = view.getUint8(o++);
            break;
         case kTString:
            array = new Array(n);
            for (; i < n; ++i)
               array[i] = this.readTString();
            return array; // exit here to avoid conflicts
         case kDouble32:
            throw new Error('kDouble32 should not be used in readFastArray');
         case kFloat16:
            throw new Error('kFloat16 should not be used in readFastArray');
         // case kBits:
         // case kUInt:
         default:
            array = new Uint32Array(n);
            for (; i < n; ++i, o += 4)
               array[i] = view.getUint32(o);
            break;
      }

      this.o = o;
      return array;
   }

   /** @summary Check if provided regions can be extracted from the buffer */
   canExtract(place) {
      for (let n = 0; n < place.length; n += 2) {
         if (place[n] + place[n + 1] > this.length)
            return false;
      }
      return true;
   }

   /** @summary Extract area */
   extract(place) {
      if (!this.arr?.buffer || !this.canExtract(place))
         return null;
      if (place.length === 2)
         return new DataView(this.arr.buffer, this.arr.byteOffset + place[0], place[1]);

      const res = new Array(place.length / 2);
      for (let n = 0; n < place.length; n += 2)
         res[n / 2] = new DataView(this.arr.buffer, this.arr.byteOffset + place[n], place[n + 1]);
      return res; // return array of buffers
   }

   /** @summary Get code at buffer position */
   codeAt(pos) { return this.arr.getUint8(pos); }

   /** @summary Get part of buffer as string */
   substring(beg, end) {
      let res = '';
      for (let n = beg; n < end; ++n)
         res += String.fromCharCode(this.arr.getUint8(n));
      return res;
   }

   /** @summary Read buffer as N-dim array */
   readNdimArray(handle, func) {
      let ndim = handle.fArrayDim, maxindx = handle.fMaxIndex, res;
      if ((ndim < 1) && (handle.fArrayLength > 0)) {
         ndim = 1;
         maxindx = [handle.fArrayLength];
      }
      if (handle.minus1)
         --ndim;

      if (ndim < 1)
         return func(this, handle);

      if (ndim === 1) {
         res = new Array(maxindx[0]);
         for (let n = 0; n < maxindx[0]; ++n)
            res[n] = func(this, handle);
      } else if (ndim === 2) {
         res = new Array(maxindx[0]);
         for (let n = 0; n < maxindx[0]; ++n) {
            const res2 = new Array(maxindx[1]);
            for (let k = 0; k < maxindx[1]; ++k)
               res2[k] = func(this, handle);
            res[n] = res2;
         }
      } else {
         const indx = new Array(ndim).fill(0), arr = new Array(ndim);
         for (let k = 0; k < ndim; ++k)
            arr[k] = [];
         res = arr[0];
         while (indx[0] < maxindx[0]) {
            let k = ndim - 1;
            arr[k].push(func(this, handle));
            ++indx[k];
            while ((indx[k] === maxindx[k]) && (k > 0)) {
               indx[k] = 0;
               arr[k - 1].push(arr[k]);
               arr[k] = [];
               ++indx[--k];
            }
         }
      }

      return res;
   }

   /** @summary read TKey data */
   readTKey(key) {
      if (!key)
         key = {};
      this.classStreamer(key, clTKey);
      const name = key.fName.replace(/['"]/g, '');
      if (name !== key.fName) {
         key.fRealName = key.fName;
         key.fName = name;
      }
      return key;
   }

   /** @summary reading basket data
     * @desc this is remaining part of TBasket streamer to decode fEntryOffset
     * after unzipping of the TBasket data */
   readBasketEntryOffset(basket, offset) {
      this.locate(basket.fLast - offset);

      if (this.remain() <= 0) {
         if (!basket.fEntryOffset && (basket.fNevBuf <= 1))
            basket.fEntryOffset = [basket.fKeylen];
         if (!basket.fEntryOffset)
            console.warn(`No fEntryOffset when expected for basket with ${basket.fNevBuf} entries`);
         return;
      }

      const nentries = this.ntoi4();
      // there is error in file=reco_103.root&item=Events;2/PCaloHits_g4SimHits_EcalHitsEE_Sim.&opt=dump;num:10;first:101
      // it is workaround, but normally I/O should fail here
      if ((nentries < 0) || (nentries > this.remain() * 4)) {
         console.error(`Error when reading entries offset from basket fNevBuf ${basket.fNevBuf} remains ${this.remain()} want to read ${nentries}`);
         if (basket.fNevBuf <= 1)
            basket.fEntryOffset = [basket.fKeylen];
         return;
      }

      basket.fEntryOffset = this.readFastArray(nentries, kInt);
      if (!basket.fEntryOffset)
         basket.fEntryOffset = [basket.fKeylen];

      if (this.remain() > 0)
         basket.fDisplacement = this.readFastArray(this.ntoi4(), kInt);
      else
         basket.fDisplacement = undefined;
   }

   /** @summary read class definition from I/O buffer */
   readClass() {
      const classInfo = { name: -1 },
            bcnt = this.ntou4(),
            startpos = this.o,
            tag = !(bcnt & kByteCountMask) || (bcnt === kNewClassTag) ? bcnt : this.ntou4();

      if (!(tag & kClassMask)) {
         classInfo.objtag = tag + this.fDisplacement; // indicate that we have deal with objects tag
         return classInfo;
      }
      if (tag === kNewClassTag) {
         // got a new class description followed by a new object
         classInfo.name = this.readNullTerminatedString();

         if (this.getMappedClass(this.fTagOffset + startpos + kMapOffset) === -1)
            this.mapClass(this.fTagOffset + startpos + kMapOffset, classInfo.name);
      } else {
         // got a tag to an already seen class
         const clTag = (tag & ~kClassMask) + this.fDisplacement;
         classInfo.name = this.getMappedClass(clTag);

         if (classInfo.name === -1)
            console.error(`Did not found class with tag ${clTag}`);
      }

      return classInfo;
   }

   /** @summary Read any object from buffer data */
   readObjectAny() {
      const objtag = this.fTagOffset + this.o + kMapOffset,
            clRef = this.readClass();

      // class identified as object and should be handled so
      if (clRef.objtag !== undefined)
         return this.getMappedObject(clRef.objtag);

      if (clRef.name === -1)
         return null;

      const arrkind = getArrayKind(clRef.name);
      let obj;

      if (arrkind === 0)
         obj = this.readTString();
      else if (arrkind > 0) {
         // reading array, can map array only afterwards
         obj = this.readFastArray(this.ntou4(), arrkind);
         this.mapObject(objtag, obj);
      } else {
         // reading normal object, should map before to
         obj = {};
         this.mapObject(objtag, obj);
         this.classStreamer(obj, clRef.name);
      }

      return obj;
   }

   /** @summary Invoke streamer for specified class  */
   classStreamer(obj, classname) {
      if (obj._typename === undefined)
         obj._typename = classname;

      const direct = DirectStreamers[classname];
      if (direct) {
         direct(this, obj);
         return obj;
      }

      const ver = this.readVersion(),
            streamer = this.fFile.getStreamer(classname, ver);

      if (streamer !== null) {
         const len = streamer.length;
         for (let n = 0; n < len; ++n)
            streamer[n].func(this, obj);
      } else {
         // just skip bytes belonging to not-recognized object
         // console.warn(`skip object ${classname}`);
         addMethods(obj);
      }

      this.checkByteCount(ver, classname);

      return obj;
   }

   /** @summary Stream class members using normal streamer */
   streamClassMembers(obj, classname, version) {
      const streamer = this.fFile.getStreamer(classname, { val: version }, undefined, true);
      if (streamer !== null) {
         const len = streamer.length;
         for (let n = 0; n < len; ++n)
            streamer[n].func(this, obj);
      }
      return obj;
   }

} // class TBuffer

// ==============================================================================

/** @summary Direct streamer for TBasket,
  * @desc uses TBuffer therefore defined later
  * @private */
DirectStreamers[clTBasket] = function(buf, obj) {
   buf.classStreamer(obj, clTKey);
   const ver = buf.readVersion();
   obj.fBufferSize = buf.ntoi4();
   obj.fNevBufSize = buf.ntoi4();

   if (obj.fNevBufSize < 0) {
      obj.fNevBufSize = -obj.fNevBufSize;
      obj.fIOBits = buf.ntoi1();
   }

   obj.fNevBuf = buf.ntoi4();
   obj.fLast = buf.ntoi4();

   if (obj.fLast > obj.fBufferSize)
      obj.fBufferSize = obj.fLast;
   const flag = buf.ntoi1();

   if (flag === 0)
      return;

   if ((flag % 10) !== 2) {
      if (obj.fNevBuf) {
         obj.fEntryOffset = buf.readFastArray(buf.ntoi4(), kInt);
         if ((flag > 20) && (flag < 40)) {
            for (let i = 0, kDisplacementMask = 0xFF000000; i < obj.fNevBuf; ++i)
               obj.fEntryOffset[i] &= ~kDisplacementMask;
         }
      }

      if (flag > 40)
         obj.fDisplacement = buf.readFastArray(buf.ntoi4(), kInt);
   }

   if ((flag === 1) || (flag > 10)) {
      // here is reading of raw data
      const sz = (ver.val <= 1) ? buf.ntoi4() : obj.fLast;

      if (sz > obj.fKeylen) {
         // buffer includes again complete TKey data - exclude it
         const blob = buf.extract([buf.o + obj.fKeylen, sz - obj.fKeylen]);
         obj.fBufferRef = new TBuffer(blob, 0, buf.fFile, sz - obj.fKeylen);
         obj.fBufferRef.fTagOffset = obj.fKeylen;
      }

      buf.shift(sz);
   }
};

// ==============================================================================

/**
  * @summary A class that reads a TDirectory from a buffer.
  *
  * @private
  */

class TDirectory {

   /** @summary constructor */
   constructor(file, dirname, cycle) {
      this.fFile = file;
      this._typename = clTDirectory;
      this.dir_name = dirname;
      this.dir_cycle = cycle;
      this.fKeys = [];
   }

   /** @summary retrieve a key by its name and cycle in the list of keys */
   getKey(keyname, cycle, only_direct) {
      if (typeof cycle !== 'number')
         cycle = -1;
      let bestkey = null;
      for (let i = 0; i < this.fKeys.length; ++i) {
         const key = this.fKeys[i];
         if (!key || (key.fName !== keyname))
            continue;
         if (key.fCycle === cycle) {
            bestkey = key;
            break;
         }
         if ((cycle < 0) && (!bestkey || (key.fCycle > bestkey.fCycle)))
            bestkey = key;
      }
      if (bestkey)
         return only_direct ? bestkey : Promise.resolve(bestkey);

      let pos = keyname.lastIndexOf('/');
      // try to handle situation when object name contains slashed (bad practice anyway)
      while (pos > 0) {
         const dirname = keyname.slice(0, pos),
               subname = keyname.slice(pos + 1),
               dirkey = this.getKey(dirname, undefined, true);

         if (dirkey && !only_direct && (dirkey.fClassName.indexOf(clTDirectory) === 0)) {
            return this.fFile.readObject(this.dir_name + '/' + dirname, 1)
                             .then(newdir => newdir.getKey(subname, cycle));
         }

         pos = keyname.lastIndexOf('/', pos - 1);
      }

      return only_direct ? null : Promise.reject(Error(`Key not found ${keyname}`));
   }

   /** @summary Read object from the directory
     * @param {string} name - object name
     * @param {number} [cycle] - cycle number
     * @return {Promise} with read object */
   readObject(obj_name, cycle) {
      return this.fFile.readObject(this.dir_name + '/' + obj_name, cycle);
   }

   /** @summary Read list of keys in directory
     * @return {Promise} with TDirectory object */
   async readKeys(objbuf) {
      objbuf.classStreamer(this, clTDirectory);

      if ((this.fSeekKeys <= 0) || (this.fNbytesKeys <= 0))
         return this;

      return this.fFile.readBuffer([this.fSeekKeys, this.fNbytesKeys]).then(blob => {
         // Read keys of the top directory

         const buf = new TBuffer(blob, 0, this.fFile);

         buf.readTKey();
         const nkeys = buf.ntoi4();

         for (let i = 0; i < nkeys; ++i)
            this.fKeys.push(buf.readTKey());

         this.fFile.fDirectories.push(this);

         return this;
      });
   }

} // class TDirectory

/**
  * @summary Interface to read objects from ROOT files
  *
  * @desc Use {@link openFile} to create instance of the class
  */

class TFile {

   constructor(url) {
      this._typename = clTFile;
      this.fEND = 0;
      this.fFullURL = url;
      this.fURL = url;
      // when disabled ('+' at the end of file name), complete file content read with single operation
      this.fAcceptRanges = true;
      // use additional time stamp parameter for file name to avoid browser caching problem
      this.fUseStampPar = settings.UseStamp ? 'stamp=' + (new Date()).getTime() : false;
      this.fFileContent = null; // this can be full or partial content of the file (if ranges are not supported or if 1K header read from file)
      // stored as TBuffer instance
      this.fMaxRanges = settings.MaxRanges || 200; // maximal number of file ranges requested at once
      this.fDirectories = [];
      this.fKeys = [];
      this.fSeekInfo = 0;
      this.fNbytesInfo = 0;
      this.fTagOffset = 0;
      this.fStreamers = 0;
      this.fStreamerInfos = null;
      this.fFileName = '';
      this.fTimeout = settings.FilesTimeout ?? 0;
      this.fStreamers = [];
      this.fBasicTypes = {}; // custom basic types, in most case enumerations

      if (!isStr(this.fURL))
         return;

      if (this.fURL.at(-1) === '+') {
         this.fURL = this.fURL.slice(0, this.fURL.length - 1);
         this.fAcceptRanges = false;
      }

      if (this.fURL.at(-1) === '^') {
         this.fURL = this.fURL.slice(0, this.fURL.length - 1);
         this.fSkipHeadRequest = true;
      }

      if (this.fURL.at(-1) === '-') {
         this.fURL = this.fURL.slice(0, this.fURL.length - 1);
         this.fUseStampPar = false;
      }

      if (this.fURL.indexOf('file://') === 0) {
         this.fUseStampPar = false;
         this.fAcceptRanges = false;
      }

      const pos = Math.max(this.fURL.lastIndexOf('/'), this.fURL.lastIndexOf('\\'));
      this.fFileName = pos >= 0 ? this.fURL.slice(pos + 1) : this.fURL;
   }

   /** @summary Set timeout for File instance
    * @desc Timeout used when submitting http requests to the server */
   setTimeout(v) {
      this.fTimeout = v;
   }

   /** @summary Assign remap for web servers
    * @desc Allows to specify fallback server if main server fails
    * @param {Object} remap - looks like { 'https://original.server/': 'https://fallback.server/' } */
   assignRemap(remap) {
      if (!remap && !isObject(remap))
         return;

      for (const key in remap) {
         if (this.fURL.indexOf(key) === 0) {
            this.fURL2 = remap[key] + this.fURL.slice(key.length);
            if (!this.fTimeout)
               this.fTimeout = 10000;
         }
      }
   }

   /** @summary Assign BufferArray with file contentOpen file
     * @private */
   assignFileContent(bufArray) {
      this.fFileContent = new TBuffer(new DataView(bufArray));
      this.fAcceptRanges = false;
      this.fUseStampPar = false;
      this.fEND = this.fFileContent.length;
   }

   /** @summary Actual file open
     * @return {Promise} when file keys are read
     * @private */
   async _open() { return this.readKeys(); }

   /** @summary read buffer(s) from the file
    * @return {Promise} with read buffers
    * @private */
   async readBuffer(place, filename, progress_callback) {
      if ((this.fFileContent !== null) && !filename && (!this.fAcceptRanges || this.fFileContent.canExtract(place)))
         return this.fFileContent.extract(place);

      let resolveFunc, rejectFunc;

      const file = this, first_block = (place[0] === 0) && (place.length === 2),
            blobs = [], // array of requested segments
            promise = new Promise((resolve, reject) => {
               resolveFunc = resolve;
               rejectFunc = reject;
            });

      let fileurl, first = 0, last = 0,
          // eslint-disable-next-line prefer-const
          read_callback, first_req,
          first_block_retry = false;

      function setFileUrl(use_second) {
         if (use_second) {
            console.log('Failure - try to repair with URL2', file.fURL2);
            internals.RemapCounter = (internals.RemapCounter ?? 0) + 1;
            file.fURL = file.fURL2;
            delete file.fURL2;
         }

         fileurl = file.fURL;
         if (isStr(filename) && filename) {
            const pos = fileurl.lastIndexOf('/');
            fileurl = (pos < 0) ? filename : fileurl.slice(0, pos + 1) + filename;
         }
      }

      function send_new_request(increment) {
         if (increment) {
            first = last;
            last = Math.min(first + file.fMaxRanges * 2, place.length);
            if (first >= place.length)
               return resolveFunc(blobs);
         }

         let fullurl = fileurl, ranges = 'bytes', totalsz = 0;
         // try to avoid browser caching by adding stamp parameter to URL
         if (file.fUseStampPar)
            fullurl += ((fullurl.indexOf('?') < 0) ? '?' : '&') + file.fUseStampPar;

         for (let n = first; n < last; n += 2) {
            ranges += (n > first ? ',' : '=') + `${place[n]}-${place[n] + place[n + 1] - 1}`;
            totalsz += place[n + 1]; // accumulated total size
         }
         if (last - first > 2)
            totalsz += (last - first) * 60; // for multi-range ~100 bytes/per request

         // when read first block, allow to read more - maybe ranges are not supported and full file content will be returned
         if (file.fAcceptRanges && first_block)
            totalsz = Math.max(totalsz, 1e7);

         return createHttpRequest(fullurl, 'buf', read_callback, undefined, true).then(xhr => {
            if (file.fAcceptRanges) {
               xhr.setRequestHeader('Range', ranges);
               xhr.expected_size = Math.max(Math.round(1.1 * totalsz), totalsz + 200); // 200 if offset for the potential gzip
            }

            if (file.fTimeout)
               xhr.timeout = file.fTimeout;

            if (isFunc(progress_callback) && isFunc(xhr.addEventListener)) {
               let sum1 = 0, sum2 = 0, sum_total = 0;
               for (let n = 1; n < place.length; n += 2) {
                  sum_total += place[n];
                  if (n < first)
                     sum1 += place[n];
                  if (n < last)
                     sum2 += place[n];
               }
               if (!sum_total)
                  sum_total = 1;

               const progress_offest = sum1 / sum_total, progress_this = (sum2 - sum1) / sum_total;
               xhr.addEventListener('progress', oEvent => {
                  if (oEvent.lengthComputable) {
                     if (progress_callback(progress_offest + progress_this * oEvent.loaded / oEvent.total) === 'break') {
                        xhr.did_abort = true;
                        xhr.abort();
                     }
                  }
               });
            } else if (first_block_retry && isFunc(xhr.addEventListener)) {
               xhr.addEventListener('progress', oEvent => {
                  if (!oEvent.total)
                     console.warn('Fail to get file size information');
                  else if (oEvent.total > 5e7) {
                     console.error(`Try to load very large file ${oEvent.total} at once - abort`);
                     xhr.did_abort = 'large';
                     xhr.abort();
                  }
               });
            }

            first_req = first_block ? xhr : null;
            xhr.send(null);
         });
      }

      read_callback = function(res) {
         if (!res && first_block) {
            // if fail to read file with stamp parameter, try once again without it
            if (file.fUseStampPar) {
               file.fUseStampPar = false;
               return send_new_request();
            }
            if (file.fURL2 && (this.did_abort !== 'large')) {
               setFileUrl(true);
               return send_new_request();
            }
            if (file.fAcceptRanges) {
               file.fAcceptRanges = false;
               first_block_retry = true;
               return send_new_request();
            }
         }

         if (res && first_req) {
            // special workaround for servers like cernbox blocking access to some response headers
            // as result, it is not possible to parse multipart responses
            if (file.fAcceptRanges && (first_req.status === 206) && (res?.byteLength === place[1]) && !first_req.getResponseHeader('Content-Range') && (file.fMaxRanges > 1)) {
               console.warn('Server response with 206 code but browser does not provide access to Content-Range header - setting fMaxRanges = 1, consider to load full file with "filename.root+" argument or adjust server configurations');
               file.fMaxRanges = 1;
            }

            // workaround for simpleHTTP
            const kind = browser.isFirefox ? first_req.getResponseHeader('Server') : '';
            if (isStr(kind) && kind.indexOf('SimpleHTTP') === 0) {
               file.fMaxRanges = 1;
               file.fUseStampPar = false;
            }
         }

         if (res && first_block && !file.fFileContent) {
            // special case - keep content of first request (could be complete file) in memory
            file.fFileContent = new TBuffer(isStr(res) ? res : new DataView(res));

            if (!file.fAcceptRanges)
               file.fEND = file.fFileContent.length;

            return resolveFunc(file.fFileContent.extract(place));
         }

         if (!res) {
            if (file.fURL2 && (this.did_abort !== 'large')) {
               setFileUrl(true);
               return send_new_request();
            }
            if ((first === 0) && (last > 2) && (file.fMaxRanges > 1)) {
               // server return no response with multi request - try to decrease ranges count or fail
               if (last / 2 > 200)
                  file.fMaxRanges = 200;
               else if (last / 2 > 50)
                  file.fMaxRanges = 50;
               else if (last / 2 > 20)
                  file.fMaxRanges = 20;
               else if (last / 2 > 5)
                  file.fMaxRanges = 5;
               else
                  file.fMaxRanges = 1;
               last = Math.min(last, file.fMaxRanges * 2);
               return send_new_request();
            }
            return rejectFunc(Error(`Fail to read with ${place.length / 2} ranges max = ${file.fMaxRanges}`));
         }

         // if only single segment requested, return result as is
         if (last - first === 2) {
            const b = new DataView(res);
            if (place.length === 2)
               return resolveFunc(b);
            blobs.push(b);
            return send_new_request(true);
         }

         // object to access response data
         const hdr = this.getResponseHeader('Content-Type'),
               ismulti = isStr(hdr) && (hdr.indexOf('multipart') >= 0),
               view = new DataView(res);

         if (!ismulti) {
            // server may returns simple buffer, which combines all segments together

            const hdr_range = this.getResponseHeader('Content-Range');
            let segm_start = 0, segm_last = -1;

            if (isStr(hdr_range) && hdr_range.indexOf('bytes') >= 0) {
               const parts = hdr_range.slice(hdr_range.indexOf('bytes') + 6).split(/[\s-/]+/);
               if (parts.length === 3) {
                  segm_start = Number.parseInt(parts[0]);
                  segm_last = Number.parseInt(parts[1]);
                  if (!Number.isInteger(segm_start) || !Number.isInteger(segm_last) || (segm_start > segm_last)) {
                     segm_start = 0;
                     segm_last = -1;
                  }
               }
            }

            let canbe_single_segment = (segm_start <= segm_last);
            for (let n = first; n < last; n += 2) {
               if ((place[n] < segm_start) || (place[n] + place[n + 1] - 1 > segm_last))
                  canbe_single_segment = false;
            }

            if (canbe_single_segment) {
               for (let n = first; n < last; n += 2)
                  blobs.push(new DataView(res, place[n] - segm_start, place[n + 1]));
               return send_new_request(true);
            }

            if ((file.fMaxRanges === 1) || !first)
               return rejectFunc(Error('Server returns normal response when multipart was requested, disable multirange support'));

            file.fMaxRanges = 1;
            last = Math.min(last, file.fMaxRanges * 2);

            return send_new_request();
         }

         // multipart messages requires special handling

         const indx = hdr.indexOf('boundary=');
         let boundary = '', n = first, o = 0, normal_order = true;
         if (indx > 0) {
            boundary = hdr.slice(indx + 9);
            if ((boundary[0] === '"') && (boundary.at(-1) === '"'))
               boundary = boundary.slice(1, boundary.length - 1);
            boundary = '--' + boundary;
         } else
            console.error('Did not found boundary id in the response header');

         while (n < last) {
            let code1, code2 = view.getUint8(o), nline = 0, line = '',
                finish_header = false, segm_start = 0, segm_last = -1;

            while ((o < view.byteLength - 1) && !finish_header && (nline < 5)) {
               code1 = code2;
               code2 = view.getUint8(o + 1);

               if (((code1 === 13) && (code2 === 10)) || (code1 === 10)) {
                  if ((line.length > 2) && (line.slice(0, 2) === '--') && (line !== boundary))
                     return rejectFunc(Error(`Decode multipart message, expect boundary ${boundary} got ${line}`));

                  line = line.toLowerCase();

                  if ((line.indexOf('content-range') >= 0) && (line.indexOf('bytes') > 0)) {
                     const parts = line.slice(line.indexOf('bytes') + 6).split(/[\s-/]+/);
                     if (parts.length === 3) {
                        segm_start = Number.parseInt(parts[0]);
                        segm_last = Number.parseInt(parts[1]);
                        if (!Number.isInteger(segm_start) || !Number.isInteger(segm_last) || (segm_start > segm_last)) {
                           segm_start = 0;
                           segm_last = -1;
                        }
                     } else
                        console.error(`Fail to decode content-range ${line} ${parts}`);
                  }

                  if ((nline > 1) && !line)
                     finish_header = true;

                  nline++;
                  line = '';
                  if (code1 !== 10) {
                     o++;
                     code2 = view.getUint8(o + 1);
                  }
               } else
                  line += String.fromCharCode(code1);
               o++;
            }

            if (!finish_header)
               return rejectFunc(Error('Cannot decode header in multipart message'));

            if (segm_start > segm_last) {
               // fall-back solution, believe that segments same as requested
               blobs.push(new DataView(res, o, place[n + 1]));
               o += place[n + 1];
               n += 2;
            } else if (normal_order) {
               const n0 = n;
               while ((n < last) && (place[n] >= segm_start) && (place[n] + place[n + 1] - 1 <= segm_last)) {
                  blobs.push(new DataView(res, o + place[n] - segm_start, place[n + 1]));
                  n += 2;
               }

               if (n > n0)
                  o += (segm_last - segm_start + 1);
               else
                  normal_order = false;
            }

            if (!normal_order) {
               // special situation when server reorder segments in the reply
               let isany = false;
               for (let n1 = n; n1 < last; n1 += 2) {
                  if ((place[n1] >= segm_start) && (place[n1] + place[n1 + 1] - 1 <= segm_last)) {
                     blobs[n1 / 2] = new DataView(res, o + place[n1] - segm_start, place[n1 + 1]);
                     isany = true;
                  }
               }
               if (!isany)
                  return rejectFunc(Error(`Provided fragment ${segm_start} - ${segm_last} out of requested multi-range request`));

               while (blobs[n / 2])
                  n += 2;

               o += (segm_last - segm_start + 1);
            }
         }

         send_new_request(true);
      };

      setFileUrl();

      return send_new_request(true).then(() => promise);
   }

   /** @summary Returns file name */
   getFileName() { return this.fFileName; }

   /** @summary Get directory with given name and cycle
    * @desc Function only can be used for already read directories, which are preserved in the memory
    * @private */
   getDir(dirname, cycle) {
      if ((cycle === undefined) && isStr(dirname)) {
         const pos = dirname.lastIndexOf(';');
         if (pos > 0) {
            cycle = Number.parseInt(dirname.slice(pos + 1));
            dirname = dirname.slice(0, pos);
         }
      }

      for (let j = 0; j < this.fDirectories.length; ++j) {
         const dir = this.fDirectories[j];
         if (dir.dir_name !== dirname)
            continue;
         if ((cycle !== undefined) && (dir.dir_cycle !== cycle))
            continue;
         return dir;
      }
      return null;
   }

   /** @summary Retrieve a key by its name and cycle in the list of keys
    * @desc If only_direct not specified, returns Promise while key keys must be read first from the directory
    * @private */
   getKey(keyname, cycle, only_direct) {
      if (typeof cycle !== 'number')
         cycle = -1;
      let bestkey = null;
      for (let i = 0; i < this.fKeys.length; ++i) {
         const key = this.fKeys[i];
         if (!key || (key.fName !== keyname))
            continue;
         if (key.fCycle === cycle) {
            bestkey = key;
            break;
         }
         if ((cycle < 0) && (!bestkey || (key.fCycle > bestkey.fCycle)))
            bestkey = key;
      }
      if (bestkey)
         return only_direct ? bestkey : Promise.resolve(bestkey);

      let pos = keyname.lastIndexOf('/');
      // try to handle situation when object name contains slashes (bad practice anyway)
      while (pos > 0) {
         const dirname = keyname.slice(0, pos),
               subname = keyname.slice(pos + 1),
               dir = this.getDir(dirname);

         if (dir)
            return dir.getKey(subname, cycle, only_direct);

         const dirkey = this.getKey(dirname, undefined, true);
         if (dirkey && !only_direct && (dirkey.fClassName.indexOf(clTDirectory) === 0))
            return this.readObject(dirname).then(newdir => newdir.getKey(subname, cycle));

         pos = keyname.lastIndexOf('/', pos - 1);
      }

      return only_direct ? null : Promise.reject(Error(`Key not found ${keyname}`));
   }

   /** @summary Read and inflate object buffer described by its key
    * @private */
   async readObjBuffer(key) {
      return this.readBuffer([key.fSeekKey + key.fKeylen, key.fNbytes - key.fKeylen]).then(blob1 => {
         if (key.fObjlen <= key.fNbytes - key.fKeylen) {
            const buf = new TBuffer(blob1, 0, this);
            buf.fTagOffset = key.fKeylen;
            return buf;
         }

         return R__unzip(blob1, key.fObjlen).then(objbuf => {
            if (!objbuf)
               return Promise.reject(Error(`Fail to UNZIP buffer for ${key.fName}`));

            const buf = new TBuffer(objbuf, 0, this);
            buf.fTagOffset = key.fKeylen;
            return buf;
         });
      });
   }

   /** @summary Read any object from a root file
     * @desc One could specify cycle number in the object name or as separate argument
     * @param {string} obj_name - name of object, may include cycle number like 'hpxpy;1'
     * @param {number} [cycle] - cycle number, also can be included in obj_name
     * @return {Promise} promise with object read
     * @example
     * import { openFile } from 'https://root.cern/js/latest/modules/io.mjs';
     * let f = await openFile('https://root.cern/js/files/hsimple.root');
     * let obj = await f.readObject('hpxpy;1');
     * console.log(`Read object of type ${obj._typename}`); */
   async readObject(obj_name, cycle, only_dir) {
      const pos = obj_name.lastIndexOf(';');
      if (pos >= 0) {
         cycle = Number.parseInt(obj_name.slice(pos + 1));
         obj_name = obj_name.slice(0, pos);
      }

      if (typeof cycle !== 'number')
         cycle = -1;
      // remove leading slashes
      while (obj_name.length && (obj_name[0] === '/'))
         obj_name = obj_name.slice(1);

      // one uses Promises while in some cases we need to
      // read sub-directory to get list of keys
      // in such situation calls are asynchronous
      return this.getKey(obj_name, cycle).then(key => {
         if ((obj_name === nameStreamerInfo) && (key.fClassName === clTList))
            return this.fStreamerInfos;

         let isdir = false;

         if ((key.fClassName === clTDirectory || key.fClassName === clTDirectoryFile)) {
            const dir = this.getDir(obj_name, cycle);
            if (dir)
               return dir;
            isdir = true;
         }

         if (!isdir && only_dir)
            return Promise.reject(Error(`Key ${obj_name} is not directory}`));

         return this.readObjBuffer(key).then(buf => {
            if (isdir) {
               const dir = new TDirectory(this, obj_name, cycle);
               dir.fTitle = key.fTitle;
               return dir.readKeys(buf);
            }

            const obj = {};
            buf.mapObject(1, obj); // tag object itself with id == 1
            buf.classStreamer(obj, key.fClassName);

            if ((key.fClassName === clTF1) || (key.fClassName === clTF12) || (key.fClassName === clTF2) || (key.fClassName === clTF3))
               return this.#readFormulas(obj);

            return obj;
         });
      });
   }

   /** @summary read formulas from the file and add them to TF1/TF2 objects
     * @private */
   async #readFormulas(tf1) {
      const arr = [];
      for (let indx = 0; indx < this.fKeys.length; ++indx) {
         if (this.fKeys[indx].fClassName === 'TFormula')
            arr.push(this.readObject(this.fKeys[indx].fName, this.fKeys[indx].fCycle));
      }

      return Promise.all(arr).then(formulas => {
         formulas.forEach(obj => tf1.addFormula(obj));
         return tf1;
      });
   }

   /** @summary extract streamer infos from the buffer
     * @private */
   extractStreamerInfos(buf) {
      if (!buf)
         return;

      const lst = {};
      buf.mapObject(1, lst);

      try {
         buf.classStreamer(lst, clTList);
      } catch (err) {
         console.error('Fail extract streamer infos', err);
         return;
      }

      lst._typename = clTStreamerInfoList;

      this.fStreamerInfos = lst;

      if (isFunc(internals.addStreamerInfosForPainter))
         internals.addStreamerInfosForPainter(lst);

      for (let k = 0; k < lst.arr.length; ++k) {
         const si = lst.arr[k];
         if (!si.fElements)
            continue;
         for (let l = 0; l < si.fElements.arr.length; ++l) {
            const elem = si.fElements.arr[l];
            if (!elem.fTypeName || !elem.fType)
               continue;

            let typ = elem.fType, typname = elem.fTypeName;

            if (typ >= 60) {
               if ((typ === kStreamer) && (elem._typename === clTStreamerSTL) && elem.fSTLtype && elem.fCtype && (elem.fCtype < 20)) {
                  const prefix = (StlNames[elem.fSTLtype] || 'undef') + '<';
                  if ((typname.indexOf(prefix) === 0) && (typname.at(-1) === '>')) {
                     typ = elem.fCtype;
                     typname = typname.slice(prefix.length, typname.length - 1).trim();

                     if ((elem.fSTLtype === kSTLmap) || (elem.fSTLtype === kSTLmultimap)) {
                        if (typname.indexOf(',') > 0)
                           typname = typname.slice(0, typname.indexOf(',')).trim();
                        else
                           continue;
                     }
                  }
               }
               if (typ >= 60)
                  continue;
            } else {
               if ((typ > 20) && (typname.at(-1) === '*'))
                  typname = typname.slice(0, typname.length - 1);
               typ %= 20;
            }

            const kind = getTypeId(typname);
            if ((kind === typ) ||
                ((typ === kBits) && (kind === kUInt)) ||
                ((typ === kCounter) && (kind === kInt)))
               continue;

            if (typname && typ && (this.fBasicTypes[typname] !== typ))
               this.fBasicTypes[typname] = typ;
         }
      }
   }

   /** @summary Read file keys
     * @private */
   async readKeys() {
      // with the first readbuffer we read bigger amount to create header cache
      return this.readBuffer([0, 400]).then(blob => {
         const buf = new TBuffer(blob, 0, this);
         if (buf.substring(0, 4) !== 'root')
            return Promise.reject(Error(`Not a ROOT file ${this.fURL}`));

         buf.shift(4);

         this.fVersion = buf.ntou4();
         this.fBEGIN = buf.ntou4();
         if (this.fVersion < 1000000) { // small file
            this.fEND = buf.ntou4();
            this.fSeekFree = buf.ntou4();
            this.fNbytesFree = buf.ntou4();
            buf.shift(4); // const nfree = buf.ntoi4();
            this.fNbytesName = buf.ntou4();
            this.fUnits = buf.ntou1();
            this.fCompress = buf.ntou4();
            this.fSeekInfo = buf.ntou4();
            this.fNbytesInfo = buf.ntou4();
         } else { // new format to support large files
            this.fEND = buf.ntou8();
            this.fSeekFree = buf.ntou8();
            this.fNbytesFree = buf.ntou4();
            buf.shift(4); // const nfree = buf.ntou4();
            this.fNbytesName = buf.ntou4();
            this.fUnits = buf.ntou1();
            this.fCompress = buf.ntou4();
            this.fSeekInfo = buf.ntou8();
            this.fNbytesInfo = buf.ntou4();
         }

         // empty file
         if (!this.fSeekInfo || !this.fNbytesInfo)
            return Promise.reject(Error(`File ${this.fURL} does not provide streamer infos`));

         // extra check to prevent reading of corrupted data
         if (!this.fNbytesName || this.fNbytesName > 100000)
            return Promise.reject(Error(`Cannot read directory info of the file ${this.fURL}`));

         // *-*-------------Read directory info
         let nbytes = this.fNbytesName + 22;
         nbytes += 4;  // fDatimeC.Sizeof();
         nbytes += 4;  // fDatimeM.Sizeof();
         nbytes += 18; // fUUID.Sizeof();
         // assume that the file may be above 2 Gbytes if file version is > 4
         if (this.fVersion >= 40000)
            nbytes += 12;

         // this part typically read from the header, no need to optimize
         return this.readBuffer([this.fBEGIN, Math.max(300, nbytes)]);
      }).then(blob3 => {
         const buf3 = new TBuffer(blob3, 0, this);

         // keep only title from TKey data
         this.fTitle = buf3.readTKey().fTitle;

         buf3.locate(this.fNbytesName);

         // we read TDirectory part of TFile
         buf3.classStreamer(this, clTDirectory);

         if (!this.fSeekKeys)
            return Promise.reject(Error(`Empty keys list in ${this.fURL}`));

         // read with same request keys and streamer infos
         return this.readBuffer([this.fSeekKeys, this.fNbytesKeys, this.fSeekInfo, this.fNbytesInfo]);
      }).then(blobs => {
         const buf4 = new TBuffer(blobs[0], 0, this);

         buf4.readTKey();
         const nkeys = buf4.ntoi4();
         for (let i = 0; i < nkeys; ++i)
            this.fKeys.push(buf4.readTKey());

         const buf5 = new TBuffer(blobs[1], 0, this),
               si_key = buf5.readTKey();
         if (!si_key)
            return Promise.reject(Error(`Fail to read StreamerInfo data in ${this.fURL}`));

         this.fKeys.push(si_key);
         return this.readObjBuffer(si_key);
      }).then(blob6 => {
         this.extractStreamerInfos(blob6);
         return this;
      });
   }

   /** @summary Read the directory content from  a root file
     * @desc If directory was already read - return previously read object
     * Same functionality as {@link TFile#readObject}
     * @param {string} dir_name - directory name
     * @param {number} [cycle] - directory cycle
     * @return {Promise} - promise with read directory */
   async readDirectory(dir_name, cycle) {
      return this.readObject(dir_name, cycle, true);
   }

   /** @summary Search streamer info
     * @param {string} clanme - class name
     * @param {number} [clversion] - class version
     * @param {number} [checksum] - streamer info checksum, have to match when specified
     * @private */
   findStreamerInfo(clname, clversion, checksum) {
      if (!this.fStreamerInfos)
         return null;

      const arr = this.fStreamerInfos.arr, len = arr.length;

      if (checksum !== undefined) {
         let cache = this.fStreamerInfos.cache;
         if (!cache)
            cache = this.fStreamerInfos.cache = {};
         let si = cache[checksum];
         if (si && (!clname || (si.fName === clname)))
            return si;

         for (let i = 0; i < len; ++i) {
            si = arr[i];
            if (si.fCheckSum === checksum) {
               cache[checksum] = si;
               if (!clname || (si.fName === clname))
                  return si;
            }
         }
         cache[checksum] = null; // checksum did not found, do not try again
      }

      if (clname) {
         for (let i = 0; i < len; ++i) {
            const si = arr[i];
            if ((si.fName === clname) && ((si.fClassVersion === clversion) || (clversion === undefined)))
               return si;
         }
      }

      return null;
   }

   /** @summary Returns streamer for the class 'clname',
     * @desc From the list of streamers or generate it from the streamer infos and add it to the list
     * @private */
   getStreamer(clname, ver, s_i, only_plain) {
      // these are special cases, which are handled separately
      if (clname === clTQObject || clname === clTBasket)
         return null;

      let fullname = clname;

      if (ver) {
         fullname += (ver.checksum ? `$chksum${ver.checksum}` : `$ver${ver.val}`);
         const streamer = this.fStreamers[fullname];
         if (streamer !== undefined)
            return streamer;
      }

      const custom = only_plain ? null : CustomStreamers[clname];

      // one can define in the user streamers just aliases
      if (isStr(custom))
         return this.getStreamer(custom, ver, s_i);

      // streamer is just separate function
      if (isFunc(custom))
         return addClassMethods(clname, [{ typename: clname, func: custom }]);

      const streamer = [];

      if (isObject(custom)) {
         if (!custom.name && !custom.func)
            return custom;
         streamer.push(custom); // special read entry, add in the beginning of streamer
      }

      // check element in streamer infos, one can have special cases
      if (!s_i)
         s_i = this.findStreamerInfo(clname, ver.val, ver.checksum);

      if (!s_i) {
         delete this.fStreamers[fullname];
         if (!ver.nowarning)
            console.warn(`Not found streamer for ${clname} ver ${ver.val} checksum ${ver.checksum} full ${fullname}`);
         return null;
      }

      // special handling for TStyle which has duplicated member name fLineStyle
      if (s_i.fName === clTStyle) {
         s_i.fElements?.arr.forEach(elem => {
            if (elem.fName === 'fLineStyle')
               elem.fName = 'fLineStyles'; // like in ROOT JSON now
         });
      }

      // for each entry in streamer info produce member function
      s_i.fElements?.arr.forEach(elem => streamer.push(createMemberStreamer(elem, this)));

      this.fStreamers[fullname] = streamer;

      return addClassMethods(clname, streamer);
   }

   /** @summary Here we produce list of members, resolving all base classes
     * @private */
   getSplittedStreamer(streamer, tgt) {
      if (!streamer)
         return tgt;

      if (!tgt)
         tgt = [];

      for (let n = 0; n < streamer.length; ++n) {
         const elem = streamer[n];

         if (elem.base === undefined) {
            tgt.push(elem);
            continue;
         }

         if (elem.basename === clTObject) {
            tgt.push({
               func(buf, obj) {
                  buf.ntoi2(); // read version, why it here??
                  obj.fUniqueID = buf.ntou4();
                  obj.fBits = buf.ntou4();
                  if (obj.fBits & kIsReferenced)
                     buf.ntou2(); // skip pid
               }
            });
            continue;
         }

         const ver = { val: elem.base };

         if (ver.val === 4294967295) {
            // this is -1 and indicates foreign class, need more workarounds
            ver.val = 1; // need to search version 1 - that happens when several versions of foreign class exists ???
         }

         const parent = this.getStreamer(elem.basename, ver);
         if (parent)
            this.getSplittedStreamer(parent, tgt);
      }

      return tgt;
   }

   /** @summary Fully cleanup TFile data
     * @private */
   delete() {
      this.fDirectories = null;
      this.fKeys = null;
      this.fStreamers = null;
      this.fSeekInfo = 0;
      this.fNbytesInfo = 0;
      this.fTagOffset = 0;
   }

} // class TFile


/** @summary Reconstruct ROOT object from binary buffer
  * @desc Method can be used to reconstruct ROOT objects from binary buffer
  * which can be requested from running `THttpServer`, using **root.bin** request
  * To decode data, one has to request streamer infos data __after__ object data
  * as it shown in example.
  *
  * Method provided for convenience only to see how binary IO works.
  * It is strongly recommended to use **root.json** request to get data directly in
  * JSON format
  *
  * @param {string} class_name - Class name of the object
  * @param {binary} obj_rawdata - data of object root.bin request
  * @param {binary} sinfo_rawdata - data of streamer info root.bin request
  * @return {object} - created JavaScript object
  * @example
  *
  * import { httpRequest } from 'jsroot/core';
  * import { reconstructObject } from 'jsroot/io';
  *
  * const obj_data = await httpRequest('http://localhost:8080/Files/job1.root/hpx/root.bin', 'buf');
  * const si_data = await httpRequest('http://localhost:8080/StreamerInfo/root.bin', 'buf');
  * const histo = await reconstructObject('TH1F', obj_data, si_data);
  * console.log(`Get histogram with title = ${histo.fTitle}`);
  *
  * // request same data via root.json request
  * httpRequest('http://localhost:8080/Files/job1.root/hpx/root.json', 'object')
  *            .then(histo => console.log(`Get histogram with title = ${histo.fTitle}`)); */
function reconstructObject(class_name, obj_rawdata, sinfo_rawdata) {
   const file = new TFile(),
         buf1 = new TBuffer(sinfo_rawdata, 0, file),
         buf2 = new TBuffer(obj_rawdata, 0, file),
         obj = {};

   file.extractStreamerInfos(buf1);
   buf2.mapObject(obj, 1);
   buf2.classStreamer(obj, class_name);

   return obj;
}

// =============================================================

/**
  * @summary Interface to read local file in the browser
  *
  * @hideconstructor
  * @desc Use {@link openFile} to create instance of the class
  * @private
  */

class TLocalFile extends TFile {

   constructor(file) {
      super(null);
      this.fUseStampPar = false;
      this.fLocalFile = file;
      this.fEND = file.size;
      this.fFullURL = file.name;
      this.fURL = file.name;
      this.fFileName = file.name;
   }

   /** @summary Open local file
     * @return {Promise} after file keys are read */
   async _open() { return this.readKeys(); }

   /** @summary read buffer from local file
     * @return {Promise} with read data */
   async readBuffer(place, filename /* , progress_callback */) {
      const file = this.fLocalFile;

      return new Promise((resolve, reject) => {
         if (filename) {
            reject(Error(`Cannot access other local file ${filename}`));
            return;
         }

         const reader = new FileReader(), blobs = [];
         let cnt = 0;

         reader.onload = function(evnt) {
            const res = new DataView(evnt.target.result);
            if (place.length === 2) {
               resolve(res);
               return;
            }

            blobs.push(res);
            cnt += 2;
            if (cnt >= place.length) {
               resolve(blobs);
               return;
            }
            reader.readAsArrayBuffer(file.slice(place[cnt], place[cnt] + place[cnt + 1]));
         };

         reader.readAsArrayBuffer(file.slice(place[0], place[0] + place[1]));
      });
   }

} // class TLocalFile

/**
  * @summary Interface to read file in node.js
  *
  * @hideconstructor
  * @desc Use {@link openFile} to create instance of the class
  * @private
  */

class TNodejsFile extends TFile {

   constructor(filename) {
      super(null);
      this.fUseStampPar = false;
      this.fEND = 0;
      this.fFullURL = filename;
      this.fURL = filename;
      this.fFileName = filename;
   }

   /** @summary Open file in node.js
     * @return {Promise} after file keys are read */
   async _open() {
      return import('fs').then(fs => {
         this.fs = fs;

         return new Promise((resolve, reject) => {
            this.fs.open(this.fFileName, 'r', (status, fd) => {
               if (status) {
                  console.log(status.message);
                  reject(Error(`Not possible to open ${this.fFileName} inside node.js`));
               } else {
                  const stats = this.fs.fstatSync(fd);
                  this.fEND = stats.size;
                  this.fd = fd;
                  this.readKeys().then(resolve).catch(reject);
               }
            });
         });
      });
   }

   /** @summary Read buffer from node.js file
     * @return {Promise} with requested blocks */
   async readBuffer(place, filename /* , progress_callback */) {
      return new Promise((resolve, reject) => {
         if (filename) {
            reject(Error(`Cannot access other local file ${filename}`));
            return;
         }

         if (!this.fs || !this.fd) {
            reject(Error(`File is not opened ${this.fFileName}`));
            return;
         }

         const blobs = [];
         let cnt = 0;

         const readfunc = (_err, _bytesRead, buf) => {
            const res = new DataView(buf.buffer, buf.byteOffset, place[cnt + 1]);
            if (place.length === 2)
               return resolve(res);
            blobs.push(res);
            cnt += 2;
            if (cnt >= place.length)
               return resolve(blobs);
            this.fs.read(this.fd, Buffer.alloc(place[cnt + 1]), 0, place[cnt + 1], place[cnt], readfunc);
         };

         this.fs.read(this.fd, Buffer.alloc(place[1]), 0, place[1], place[0], readfunc);
      });
   }

} // class TNodejsFile

/**
  * @summary Proxy to read file content
  *
  * @desc Should implement following methods:
  *
  * - openFile() - return Promise with true when file can be open normally
  * - getFileName() - returns string with file name
  * - getFileSize() - returns size of file
  * - readBuffer(pos, len) - return promise with DataView for requested position and length
  *
  * @private
  */

class FileProxy {

   async openFile() { return false; }
   getFileName() { return ''; }
   getFileSize() { return 0; }
   async readBuffer(/* pos, sz */) { return null; }

} // class FileProxy

/**
  * @summary File to use file context via FileProxy
  *
  * @hideconstructor
  * @desc Use {@link openFile} to create instance of the class, providing FileProxy as argument
  * @private
  */

class TProxyFile extends TFile {

   constructor(proxy) {
      super(null);
      this.fUseStampPar = false;
      this.proxy = proxy;
   }

   /** @summary Open file
     * @return {Promise} after file keys are read */
   async _open() {
      return this.proxy.openFile().then(res => {
         if (!res)
            return false;
         this.fEND = this.proxy.getFileSize();
         this.fFullURL = this.fURL = this.fFileName = this.proxy.getFileName();
         if (isStr(this.fFileName)) {
            const p = this.fFileName.lastIndexOf('/');
            if ((p > 0) && (p < this.fFileName.length - 4))
               this.fFileName = this.fFileName.slice(p + 1);
         }
         return this.readKeys();
      });
   }

   /** @summary Read buffer from FileProxy
     * @return {Promise} with requested blocks */
   async readBuffer(place, filename /* , progress_callback */) {
      if (filename)
         return Promise.reject(Error(`Cannot access other file ${filename}`));

      if (!this.proxy)
         return Promise.reject(Error(`File is not opened ${this.fFileName}`));

      if (isFunc(this.proxy.readBuffers)) {
         return this.proxy.readBuffers(place).then(arr => {
            return arr?.length === 1 ? arr[0] : arr;
         });
      }

      if (place.length === 2)
         return this.proxy.readBuffer(place[0], place[1]);

      const arr = [];
      for (let k = 0; k < place.length; k += 2)
         arr.push(this.proxy.readBuffer(place[k], place[k + 1]));
      return Promise.all(arr);
   }

} // class TProxyFile


/** @summary Open ROOT file for reading
  * @desc Generic method to open ROOT file for reading
  * Following kind of arguments can be provided:
  *  - string with file URL (see example). In node.js environment local file like 'file://hsimple.root' can be specified
  *  - [File]{@link https://developer.mozilla.org/en-US/docs/Web/API/File} instance which let read local files from browser
  *  - [ArrayBuffer]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer} instance with complete file content
  *  - [FileProxy]{@link FileProxy} let access arbitrary files via tiny proxy API
  * @param {string|object} arg - argument for file open like url, see details
  * @param {object} [opts] - extra arguments
  * @param {Number} [opts.timeout=0] - read timeout for http requests in ms
  * @param {Object} [opts.remap={}] - http server remap to fallback when main server fails, like { 'https://original.server/': 'https://fallback.server/' }
  * @return {object} - Promise with {@link TFile} instance when file is opened
  * @example
  *
  * import { openFile } from 'https://root.cern/js/latest/modules/io.mjs';
  * let f = await openFile('https://root.cern/js/files/hsimple.root');
  * console.log(`Open file ${f.getFileName()}`); */
function openFile(arg, opts) {
   let file, plain_file;

   if (isNodeJs() && isStr(arg)) {
      if (!arg.indexOf('file://'))
         file = new TNodejsFile(arg.slice(7));
      else if (arg.indexOf('http'))
         file = new TNodejsFile(arg);
   }

   if (!file && isObject(arg) && (arg instanceof FileProxy))
      file = new TProxyFile(arg);

   if (!file && isObject(arg) && (arg instanceof ArrayBuffer)) {
      file = new TFile('localfile.root');
      file.assignFileContent(arg);
   }

   if (!file && isObject(arg) && arg.size && arg.name)
      file = new TLocalFile(arg);

   if (!file) {
      file = new TFile(arg);
      plain_file = true;
      file.assignRemap(settings.FilesRemap);
   }

   if (opts && isObject(opts)) {
      if (opts.timeout)
         file.setTimeout(opts.timeout);
      if (plain_file && opts.remap)
         file.assignRemap(opts.remap);
   }

   return file._open();
}

/** @summary Unzip JSON string
 * @desc Should be used for buffer produced with TBufferJSON::zipJSON() method
 * @param tgtsize - original length of json string
 * @param src - string with data returned by TBufferJSON::zipJSON
 * @return {Promise} with unzipped string */

async function unzipJSON(tgtsize, src) {
   const bindata = atob_func(src),
         buf = new ArrayBuffer(bindata.length),
         bufView = new DataView(buf);
   for (let i = 0; i < bindata.length; i++)
      bufView.setUint8(i, bindata.charCodeAt(i));

   return R__unzip(bufView, tgtsize).then(resView => {
      let resstr = '';
      for (let i = 0; i < tgtsize; i++)
         resstr += String.fromCharCode(resView.getUint8(i));
      return resstr;
   });
}

// special way to assign methods when streaming objects
addClassMethods(clTNamed, CustomStreamers[clTNamed]);
addClassMethods(clTObjString, CustomStreamers[clTObjString]);

export { kChar, kShort, kInt, kLong, kFloat, kCounter,
   kCharStar, kDouble, kDouble32, kLegacyChar,
   kUChar, kUShort, kUInt, kULong, kBits,
   kLong64, kULong64, kBool, kFloat16,
   kBase, kOffsetL, kOffsetP, kObject, kAny, kObjectp, kObjectP, kTString,
   kAnyP, kStreamer, kStreamLoop, kSTLp, kSTL, kBaseClass,
   clTStreamerInfoList, clTDirectory, clTDirectoryFile, nameStreamerInfo, clTBasket,
   R__unzip, unzipJSON, addUserStreamer, createStreamerElement, createMemberStreamer,
   openFile, reconstructObject, FileProxy, TBuffer };