text: const iconv = require('iconv-lite');
  const sprintf = require('sprintf-js').sprintf;
  const TYPE = require('./data-type').TYPE;
  const guidParser = require('./guid-parser');
  
  const readPrecision = require('./metadata-parser').readPrecision;
  const readScale = require('./metadata-parser').readScale;
  const readCollation = require('./metadata-parser').readCollation;
  const convertLEBytesToString = require('./tracking-buffer/bigint').convertLEBytesToString;
  
  const NULL = (1 << 16) - 1;
  const MAX = (1 << 16) - 1;
  const THREE_AND_A_THIRD = 3 + (1 / 3);
  const MONEY_DIVISOR = 10000;
  const PLP_NULL = Buffer.from([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]);
  const UNKNOWN_PLP_LEN = Buffer.from([0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]);
  const DEFAULT_ENCODING = 'utf8';
  
  function readTextPointerNull(parser, type, callback) {
    if (type.hasTextPointerAndTimestamp) {
      parser.readUInt8((textPointerLength) => {
        if (textPointerLength !== 0) {
          // Appear to be dummy values, so consume and discard them.
          parser.readBuffer(textPointerLength, () => {
            parser.readBuffer(8, () => {
              callback(undefined);
            });
          });
        } else {
          callback(true);
        }
      });
    } else {
      callback(undefined);
    }
  }
  
  function readDataLength(parser, type, metaData, textPointerNull, callback) {
    if (textPointerNull) {
      return callback(0);
    }
  
    if (metaData.isVariantValue) {
      return callback(metaData.dataLength);
    }
  
    // s2.2.4.2.1
    switch (type.id & 0x30) {
      case 0x10: // xx01xxxx - s2.2.4.2.1.1
        return callback(0);
  
      case 0x20: // xx10xxxx - s2.2.4.2.1.3
        // Variable length
        if (metaData.dataLength !== MAX) {
          switch (type.dataLengthLength) {
            case 0:
              return callback(undefined);
  
            case 1:
              return parser.readUInt8(callback);
  
            case 2:
              return parser.readUInt16LE(callback);
  
            case 4:
              return parser.readUInt32LE(callback);
  
            default:
              return parser.emit('error', new Error('Unsupported dataLengthLength ' + type.dataLengthLength + ' for data type ' + type.name));
          }
        } else {
          return callback(undefined);
        }
  
      case 0x30:
        return callback(1 << ((type.id & 0x0C) >> 2));
    }
  }
  
  module.exports = valueParse;
  function valueParse(parser, metaData, options, callback) {
    const type = metaData.type;
  
    readTextPointerNull(parser, type, (textPointerNull) => {
      readDataLength(parser, type, metaData, textPointerNull, (dataLength) => {
        switch (type.name) {
          case 'Null':
            return callback(null);
  
          case 'TinyInt':
            return parser.readUInt8(callback);
  
          case 'Int':
            return parser.readInt32LE(callback);
  
          case 'SmallInt':
            return parser.readInt16LE(callback);
  
          case 'BigInt':
            return parser.readBuffer(8, (buffer) => {
              callback(convertLEBytesToString(buffer));
            });
  
          case 'IntN':
            switch (dataLength) {
              case 0:
                return callback(null);
              case 1:
                return parser.readUInt8(callback);
              case 2:
                return parser.readInt16LE(callback);
              case 4:
                return parser.readInt32LE(callback);
              case 8:
                return parser.readBuffer(8, (buffer) => {
                  callback(convertLEBytesToString(buffer));
                });
  
              default:
                return parser.emit('error', new Error('Unsupported dataLength ' + dataLength + ' for IntN'));
            }
  
          case 'Real':
            return parser.readFloatLE(callback);
  
          case 'Float':
            return parser.readDoubleLE(callback);
  
          case 'FloatN':
            switch (dataLength) {
              case 0:
                return callback(null);
              case 4:
                return parser.readFloatLE(callback);
              case 8:
                return parser.readDoubleLE(callback);
  
              default:
                return parser.emit('error', new Error('Unsupported dataLength ' + dataLength + ' for FloatN'));
            }
  
          case 'Money':
          case 'SmallMoney':
          case 'MoneyN':
            switch (dataLength) {
              case 0:
                return callback(null);
              case 4:
                return parser.readInt32LE((value) => {
                  callback(value / MONEY_DIVISOR);
                });
              case 8:
                return parser.readInt32LE((high) => {
                  parser.readUInt32LE((low) => {
                    callback((low + (0x100000000 * high)) / MONEY_DIVISOR);
                  });
                });
  
              default:
                return parser.emit('error', new Error('Unsupported dataLength ' + dataLength + ' for MoneyN'));
            }
  
          case 'Bit':
            return parser.readUInt8((value) => {
              callback(!!value);
            });
  
          case 'BitN':
            switch (dataLength) {
              case 0:
                return callback(null);
              case 1:
                return parser.readUInt8((value) => {
                  callback(!!value);
                });
            }
  
          case 'VarChar':
          case 'Char':
            const codepage = metaData.collation.codepage;
            if (metaData.dataLength === MAX) {
              return readMaxChars(parser, codepage, callback);
            } else {
              return readChars(parser, dataLength, codepage, NULL, callback);
            }
  
          case 'NVarChar':
          case 'NChar':
            if (metaData.dataLength === MAX) {
              return readMaxNChars(parser, callback);
            } else {
              return readNChars(parser, dataLength, NULL, callback);
            }
  
          case 'VarBinary':
          case 'Binary':
            if (metaData.dataLength === MAX) {
              return readMaxBinary(parser, callback);
            } else {
              return readBinary(parser, dataLength, NULL, callback);
            }
  
          case 'Text':
            if (textPointerNull) {
              return callback(null);
            } else {
              return readChars(parser, dataLength, metaData.collation.codepage, PLP_NULL, callback);
            }
  
          case 'NText':
            if (textPointerNull) {
              return callback(null);
            } else {
              return readNChars(parser, dataLength, PLP_NULL, callback);
            }
  
          case 'Image':
            if (textPointerNull) {
              return callback(null);
            } else {
              return readBinary(parser, dataLength, PLP_NULL, callback);
            }
  
          case 'Xml':
            return readMaxNChars(parser, callback);
  
          case 'SmallDateTime':
            return readSmallDateTime(parser, options.useUTC, callback);
  
          case 'DateTime':
            return readDateTime(parser, options.useUTC, callback);
  
          case 'DateTimeN':
            switch (dataLength) {
              case 0:
                return callback(null);
              case 4:
                return readSmallDateTime(parser, options.useUTC, callback);
              case 8:
                return readDateTime(parser, options.useUTC, callback);
            }
  
          case 'Time':
            if (dataLength === 0) {
              return callback(null);
            } else {
              return readTime(parser, dataLength, metaData.scale, options.useUTC, callback);
            }
  
          case 'Date':
            if (dataLength === 0) {
              return callback(null);
            } else {
              return readDate(parser, options.useUTC, callback);
            }
  
          case 'DateTime2':
            if (dataLength === 0) {
              return callback(null);
            } else {
              return readDateTime2(parser, dataLength, metaData.scale, options.useUTC, callback);
            }
  
          case 'DateTimeOffset':
            if (dataLength === 0) {
              return callback(null);
            } else {
              return readDateTimeOffset(parser, dataLength, metaData.scale, callback);
            }
  
          case 'NumericN':
          case 'DecimalN':
            if (dataLength === 0) {
              return callback(null);
            } else {
              return parser.readUInt8((sign) => {
                sign = sign === 1 ? 1 : -1;
  
                let readValue;
                switch (dataLength - 1) {
                  case 4:
                    readValue = parser.readUInt32LE;
                    break;
                  case 8:
                    readValue = parser.readUNumeric64LE;
                    break;
                  case 12:
                    readValue = parser.readUNumeric96LE;
                    break;
                  case 16:
                    readValue = parser.readUNumeric128LE;
                    break;
                  default:
                    return parser.emit('error', new Error(sprintf('Unsupported numeric size %d', dataLength - 1)));
                }
  
                readValue.call(parser, (value) => {
                  callback((value * sign) / Math.pow(10, metaData.scale));
                });
              });
            }
  
          case 'UniqueIdentifier':
            switch (dataLength) {
              case 0:
                return callback(null);
              case 0x10:
                return parser.readBuffer(0x10, (data) => {
                  callback(guidParser.arrayToGuid(data));
                });
  
              default:
                return parser.emit('error', new Error(sprintf('Unsupported guid size %d', dataLength - 1)));
            }
  
          case 'UDT':
            return readMaxBinary(parser, callback);
  
          case 'Variant':
            if (dataLength === 0) {
              return callback(null);
            }
  
            const valueMetaData = metaData.valueMetaData = {};
            Object.defineProperty(valueMetaData, 'isVariantValue', {value: true});
            return parser.readUInt8((baseType) => {
              return parser.readUInt8((propBytes) => {
                valueMetaData.dataLength = dataLength - propBytes - 2;
                valueMetaData.type = TYPE[baseType];
                return readPrecision(parser, valueMetaData.type, (precision) => {
                  valueMetaData.precision = precision;
                  return readScale(parser, valueMetaData.type, (scale) => {
                    valueMetaData.scale = scale;
                    return readCollation(parser, valueMetaData.type, (collation) => {
                      valueMetaData.collation = collation;
                      if (baseType === 0xA5 || baseType === 0xAD || baseType === 0xA7 || baseType === 0xAF || baseType === 0xE7 || baseType === 0xEF) {
                        return readDataLength(parser, valueMetaData.type, {}, null, (maxDataLength) => {
                          // skip the 2-byte max length sent for BIGVARCHRTYPE, BIGCHARTYPE, NVARCHARTYPE, NCHARTYPE, BIGVARBINTYPE and BIGBINARYTYPE types
                          // and parse based on the length of actual data
                          return valueParse(parser, valueMetaData, options, callback);
                        });
                      } else {
                        return valueParse(parser, valueMetaData, options, callback);
                      }
                    });
                  });
                });
              });
            });
  
          default:
            return parser.emit('error', new Error(sprintf('Unrecognised type %s', type.name)));
        }
      });
    });
  }
  
  function readBinary(parser, dataLength, nullValue, callback) {
    if (dataLength === nullValue) {
      return callback(null);
    } else {
      return parser.readBuffer(dataLength, callback);
    }
  }
  
  function readChars(parser, dataLength, codepage, nullValue, callback) {
    if (codepage == null) {
      codepage = DEFAULT_ENCODING;
    }
  
    if (dataLength === nullValue) {
      return callback(null);
    } else {
      return parser.readBuffer(dataLength, (data) => {
        callback(iconv.decode(data, codepage));
      });
    }
  }
  
  function readNChars(parser, dataLength, nullValue, callback) {
    if (dataLength === nullValue) {
      return callback(null);
    } else {
      return parser.readBuffer(dataLength, (data) => {
        callback(data.toString('ucs2'));
      });
    }
  }
  
  function readMaxBinary(parser, callback) {
    return readMax(parser, callback);
  }
  
  function readMaxChars(parser, codepage, callback) {
    if (codepage == null) {
      codepage = DEFAULT_ENCODING;
    }
  
    readMax(parser, (data) => {
      if (data) {
        callback(iconv.decode(data, codepage));
      } else {
        callback(null);
      }
    });
  }
  
  function readMaxNChars(parser, callback) {
    readMax(parser, (data) => {
      if (data) {
        callback(data.toString('ucs2'));
      } else {
        callback(null);
      }
    });
  }
  
  function readMax(parser, callback) {
    parser.readBuffer(8, (type) => {
      if (type.equals(PLP_NULL)) {
        return callback(null);
      } else if (type.equals(UNKNOWN_PLP_LEN)) {
        return readMaxUnknownLength(parser, callback);
      } else {
        const low = type.readUInt32LE(0);
        const high = type.readUInt32LE(4);
  
        if (high >= (2 << (53 - 32))) {
          console.warn('Read UInt64LE > 53 bits : high=' + high + ', low=' + low);
        }
  
        const expectedLength = low + (0x100000000 * high);
        return readMaxKnownLength(parser, expectedLength, callback);
      }
    });
  }
  
  function readMaxKnownLength(parser, totalLength, callback) {
    const data = Buffer.alloc(totalLength, 0);
  
    let offset = 0;
    function next(done) {
      parser.readUInt32LE((chunkLength) => {
        if (!chunkLength) {
          return done();
        }
  
        parser.readBuffer(chunkLength, (chunk) => {
          chunk.copy(data, offset);
          offset += chunkLength;
  
          next(done);
        });
      });
    }
  
    next(() => {
      if (offset !== totalLength) {
        parser.emit('error', new Error('Partially Length-prefixed Bytes unmatched lengths : expected ' + totalLength + ', but got ' + offset + ' bytes'));
      }
  
      callback(data);
    });
  }
  
  function readMaxUnknownLength(parser, callback) {
    const chunks = [];
  
    let length = 0;
    function next(done) {
      parser.readUInt32LE((chunkLength) => {
        if (!chunkLength) {
          return done();
        }
  
        parser.readBuffer(chunkLength, (chunk) => {
          chunks.push(chunk);
          length += chunkLength;
  
          next(done);
        });
      });
    }
  
    next(() => {
      callback(Buffer.concat(chunks, length));
    });
  }
  
  function readSmallDateTime(parser, useUTC, callback) {
    parser.readUInt16LE((days) => {
      parser.readUInt16LE((minutes) => {
        let value;
        if (useUTC) {
          value = new Date(Date.UTC(1900, 0, 1 + days, 0, minutes));
        } else {
          value = new Date(1900, 0, 1 + days, 0, minutes);
        }
        callback(value);
      });
    });
  }
  
  function readDateTime(parser, useUTC, callback) {
    parser.readInt32LE((days) => {
      parser.readUInt32LE((threeHundredthsOfSecond) => {
        const milliseconds = Math.round(threeHundredthsOfSecond * THREE_AND_A_THIRD);
  
        let value;
        if (useUTC) {
          value = new Date(Date.UTC(1900, 0, 1 + days, 0, 0, 0, milliseconds));
        } else {
          value = new Date(1900, 0, 1 + days, 0, 0, 0, milliseconds);
        }
  
        callback(value);
      });
    });
  }
  
  function readTime(parser, dataLength, scale, useUTC, callback) {
    let readValue;
    switch (dataLength) {
      case 3:
        readValue = parser.readUInt24LE;
        break;
      case 4:
        readValue = parser.readUInt32LE;
        break;
      case 5:
        readValue = parser.readUInt40LE;
    }
  
    readValue.call(parser, (value) => {
      if (scale < 7) {
        for (let i = scale; i < 7; i++) {
          value *= 10;
        }
      }
  
      let date;
      if (useUTC) {
        date = new Date(Date.UTC(1970, 0, 1, 0, 0, 0, value / 10000));
      } else {
        date = new Date(1970, 0, 1, 0, 0, 0, value / 10000);
      }
      Object.defineProperty(date, 'nanosecondsDelta', {
        enumerable: false,
        value: (value % 10000) / Math.pow(10, 7)
      });
      callback(date);
    });
  }
  
  function readDate(parser, useUTC, callback) {
    parser.readUInt24LE((days) => {
      if (useUTC) {
        callback(new Date(Date.UTC(2000, 0, days - 730118)));
      } else {
        callback(new Date(2000, 0, days - 730118));
      }
    });
  }
  
  function readDateTime2(parser, dataLength, scale, useUTC, callback) {
    readTime(parser, dataLength - 3, scale, useUTC, (time) => {
      parser.readUInt24LE((days) => {
        let date;
        if (useUTC) {
          date = new Date(Date.UTC(2000, 0, days - 730118, 0, 0, 0, +time));
        } else {
          date = new Date(2000, 0, days - 730118, time.getHours(), time.getMinutes(), time.getSeconds(), time.getMilliseconds());
        }
        Object.defineProperty(date, 'nanosecondsDelta', {
          enumerable: false,
          value: time.nanosecondsDelta
        });
        callback(date);
      });
    });
  }
  
  function readDateTimeOffset(parser, dataLength, scale, callback) {
    readTime(parser, dataLength - 5, scale, true, (time) => {
      parser.readUInt24LE((days) => {
        // offset
        parser.readInt16LE(() => {
          const date = new Date(Date.UTC(2000, 0, days - 730118, 0, 0, 0, +time));
          Object.defineProperty(date, 'nanosecondsDelta', {
            enumerable: false,
            value: time.nanosecondsDelta
          });
          callback(date);
        });
      });
    });
  }
  