

// Low-level types that make up operations

var ByteBuffer = require('bytebuffer');
var Serializer = require('./serializer');
var v = require('./validation');
var ObjectId = require('./object_id')
var fp = require('./fast_parser');
var chain_types = require('./ChainTypes')
var Long = ByteBuffer.Long

import { PublicKey, Address, ecc_config } from "../../ecc"
import { toImpliedDecimal, fromImpliedDecimal } from "./number_utils"

var Types = {}
module.exports = Types

const HEX_DUMP = process.env.npm_config__graphene_serializer_hex_dump

/**
* Asset symbols contain the following information
*
*  4 bit PRECISION
*  4 bit RESERVED
*  CHAR[6] up to 6 upper case alpha numeric ascii characters,
*  char = \0  null terminated
*
*  It is treated as a uint64_t for all internal operations, but
*  is easily converted to something that can be displayed.
*/
Types.asset = {
    fromByteBuffer(b){
        let amount = b.readInt64()
        let precision = b.readUint8()
        let b_copy = b.copy(b.offset, b.offset + 7)
        let symbol = new Buffer(b_copy.toBinary(), "binary").toString().replace(/\x00/g, "")
        b.skip(7);
        // "1.000 STEEM" always written with full precision
        let amount_string = fromImpliedDecimal(amount, precision)
        return amount_string + " " + symbol
    },
    appendByteBuffer(b, object){
        object = object.trim()
        if( ! /^[0-9]+\.?[0-9]* [A-Za-z0-9]+$/.test(object))
            throw new Error("Expecting amount like '99.000 SYMBOL', instead got '" + object + "'")

        let [ amount, symbol ] = object.split(" ")
        if(symbol.length > 6)
            throw new Error("Symbols are not longer than 6 characters " + symbol + "-"+ symbol.length)

        b.writeInt64(v.to_long(amount.replace(".", "")))
        let dot = amount.indexOf(".") // 0.000
        let precision = dot === -1 ? 0 : amount.length - dot - 1
        b.writeUint8(precision)
        b.append(symbol.toUpperCase(), 'binary')
        for(let i = 0; i < 7 - symbol.length; i++)
            b.writeUint8(0)
        return
    },
    fromObject(object){
        return object
    },
    toObject(object, debug = {}){
        if (debug.use_default && object === undefined) { return "0.000 STEEM"; }
        return object
    }
}

Types.uint8 = {

    fromByteBuffer(b){
        return b.readUint8();
    },
    appendByteBuffer(b, object){
        v.require_range(0,0xFF,object, `uint8 ${object}`);
        b.writeUint8(object);
        return;
    },
    fromObject(object){
        v.require_range(0,0xFF,object, `uint8 ${object}`);
        return object;
    },
    toObject(object, debug = {}){
        if (debug.use_default && object === undefined) { return 0; }
        v.require_range(0,0xFF,object, `uint8 ${object}`);
        return parseInt(object);
    }
    };

Types.uint16 =
    {fromByteBuffer(b){
        return b.readUint16();
    },
    appendByteBuffer(b, object){
        v.require_range(0,0xFFFF,object, `uint16 ${object}`);
        b.writeUint16(object);
        return;
    },
    fromObject(object){
        v.require_range(0,0xFFFF,object, `uint16 ${object}`);
        return object;
    },
    toObject(object, debug = {}){
        if (debug.use_default && object === undefined) { return 0; }
        v.require_range(0,0xFFFF,object, `uint16 ${object}`);
        return parseInt(object);
    }
    };

Types.uint32 =
    {fromByteBuffer(b){
        return b.readUint32();
    },
    appendByteBuffer(b, object){
        v.require_range(0,0xFFFFFFFF,object, `uint32 ${object}`);
        b.writeUint32(object);
        return;
    },
    fromObject(object){
        v.require_range(0,0xFFFFFFFF,object, `uint32 ${object}`);
        return object;
    },
    toObject(object, debug = {}){
        if (debug.use_default && object === undefined) { return 0; }
        v.require_range(0,0xFFFFFFFF,object, `uint32 ${object}`);
        return parseInt(object);
    }
    };

var MIN_SIGNED_32 = -1 * Math.pow(2,31);
var MAX_SIGNED_32 = Math.pow(2,31) - 1;

Types.varint32 =
    {fromByteBuffer(b){
        return b.readVarint32();
    },
    appendByteBuffer(b, object){
        v.require_range(
            MIN_SIGNED_32,
            MAX_SIGNED_32,
            object,
            `uint32 ${object}`
        );
        b.writeVarint32(object);
        return;
    },
    fromObject(object){
        v.require_range(
            MIN_SIGNED_32,
            MAX_SIGNED_32,
            object,
            `uint32 ${object}`
        );
        return object;
    },
    toObject(object, debug = {}){
        if (debug.use_default && object === undefined) { return 0; }
        v.require_range(
            MIN_SIGNED_32,
            MAX_SIGNED_32,
            object,
            `uint32 ${object}`
        );
        return parseInt(object);
    }
    };

Types.int16 =
    {fromByteBuffer(b){
        return b.readInt16();
    },
    appendByteBuffer(b, object){
        b.writeInt16(object);
        return;
    },
    fromObject(object){
        return object;
    },
    toObject(object, debug = {}){
        if (debug.use_default && object === undefined) { return 0; }
        return parseInt(object);
    }
    };

Types.int64 =
    {fromByteBuffer(b){
        return b.readInt64();
    },
    appendByteBuffer(b, object){
        v.required(object);
        b.writeInt64(v.to_long(object));
        return;
    },
    fromObject(object){
        v.required(object);
        return v.to_long(object);
    },
    toObject(object, debug = {}){
        if (debug.use_default && object === undefined) { return "0"; }
        v.required(object);
        return v.to_long(object).toString();
    }
    };

Types.uint64 =
    {fromByteBuffer(b){
        return b.readUint64();
    },
    appendByteBuffer(b, object){
        b.writeUint64(v.to_long(v.unsigned(object)));
        return;
    },
    fromObject(object){
        return v.to_long(v.unsigned(object));
    },
    toObject(object, debug = {}){
        if (debug.use_default && object === undefined) { return "0"; }
        return v.to_long(object).toString();
    }
    };

Types.string =
    {fromByteBuffer(b){
        var b_copy;
        var len = b.readVarint32();
        b_copy = b.copy(b.offset, b.offset + len), b.skip(len);
        return new Buffer(b_copy.toBinary(), 'binary');
    },
    appendByteBuffer(b, object){
        v.required(object);
        b.writeVarint32(object.length);
        b.append(object.toString('binary'), 'binary');
        return;
    },
    fromObject(object){
        v.required(object);
        return new Buffer(object);
    },
    toObject(object, debug = {}){
        if (debug.use_default && object === undefined) { return ""; }
        return object.toString();
    }
    };

Types.bytes = function(size){
    return {fromByteBuffer(b){
        if (size === undefined) {
            var b_copy;
            var len = b.readVarint32();
            b_copy = b.copy(b.offset, b.offset + len), b.skip(len);
            return new Buffer(b_copy.toBinary(), 'binary');
        } else {
            b_copy = b.copy(b.offset, b.offset + size), b.skip(size);
            return new Buffer(b_copy.toBinary(), 'binary');
        }
    },
    appendByteBuffer(b, object){
        v.required(object);
        if(typeof object === "string")
            object = new Buffer(object, "hex")

        if (size === undefined) {
            b.writeVarint32(object.length);
        }
        b.append(object.toString('binary'), 'binary');
        return;
    },
    fromObject(object){
        v.required(object);
        if( Buffer.isBuffer(object) )
            return object

        return new Buffer(object, 'hex');
    },
    toObject(object, debug = {}){
        if (debug.use_default && object === undefined) {
            var zeros=function(num){ return new Array( num ).join( "00" ); };
            return zeros(size);
        }
        v.required(object);
        return object.toString('hex');
    }
    };
};

Types.bool =
    {fromByteBuffer(b){
        return b.readUint8() === 1
    },
    appendByteBuffer(b, object){
        // supports boolean or integer
        b.writeUint8(JSON.parse(object) ? 1 : 0);
        return;
    },
    fromObject(object){
        return JSON.parse(object) ? true : false
    },
    toObject(object, debug = {}){
        if (debug.use_default && object === undefined) { return false; }
        return JSON.parse(object) ? true : false
    }
};

Types.void =
    {fromByteBuffer(b){
        throw new Error("(void) undefined type");
    },
    appendByteBuffer(b, object){
        throw new Error("(void) undefined type");
    },
    fromObject(object){
        throw new Error("(void) undefined type");
    },
    toObject(object, debug = {}){
        if (debug.use_default && object === undefined) {
            return undefined;
        }
        throw new Error("(void) undefined type");
    }
    };

Types.array = function(st_operation){
    return {fromByteBuffer(b){
        var size = b.readVarint32();
        if (HEX_DUMP) {
            console.log("varint32 size = " + size.toString(16));
        }
        var result = [];
        for (var i = 0; 0 < size ? i < size : i > size; 0 < size ? i++ : i++) {
            result.push(st_operation.fromByteBuffer(b));
        }
        return sortOperation(result, st_operation);
    },
    appendByteBuffer(b, object){
        v.required(object)
        object = sortOperation(object, st_operation)
        b.writeVarint32(object.length);
        for (var i = 0, o; i < object.length; i++) {
            o = object[i];
            st_operation.appendByteBuffer(b, o);
        }
    },
    fromObject(object){
        v.required(object)
        object = sortOperation(object, st_operation)
        var result = [];
        for (var i = 0, o; i < object.length; i++) {
            o = object[i];
            result.push(st_operation.fromObject(o));
        }
        return result;
    },
    toObject(object, debug = {}){
        if (debug.use_default && object === undefined) {
            return [ st_operation.toObject(object, debug) ];
        }
        v.required(object)
        object = sortOperation(object, st_operation)

        var result = [];
        for (var i = 0, o; i < object.length; i++) {
            o = object[i];
            result.push(st_operation.toObject(o, debug));
        }
        return result;
    }
    };
};

Types.time_point_sec = {
    fromByteBuffer(b){ return b.readUint32(); },
    appendByteBuffer(b, object){
        if(typeof object !== "number")
            object = Types.time_point_sec.fromObject(object)

        b.writeUint32(object);
        return;
    },
    fromObject(object){
        v.required(object)

        if(typeof object === "number")
            return object

        if(object.getTime)
            return Math.floor( object.getTime() / 1000 );

        if(typeof object !== "string")
            throw new Error("Unknown date type: " + object)

        // if(typeof object === "string" && !/Z$/.test(object))
        //     object = object + "Z"

        return Math.floor( new Date(object).getTime() / 1000 );
    },
    toObject(object, debug = {}){
        if (debug.use_default && object === undefined)
            return (new Date(0)).toISOString().split('.')[0];

        v.required(object)

        if(typeof object === "string")
            return object

        if(object.getTime)
            return object.toISOString().split('.')[0]

        var int = parseInt(object);
        v.require_range(0,0xFFFFFFFF,int, `uint32 ${object}`);
        return (new Date( int * 1000 )).toISOString().split('.')[0];
    }
}

Types.set = function(st_operation){
    return {validate(array){
        var dup_map = {};
        for (var i = 0, o; i < array.length; i++) {
            o = array[i];
            var ref;
            if (ref = typeof o, ['string', 'number'].indexOf(ref) >= 0) {
                if (dup_map[o] !== undefined) {
                    throw new Error("duplicate (set)");
                }
                dup_map[o] = true;
            }
        }
        return sortOperation(array, st_operation);
    },
    fromByteBuffer(b){
        var size = b.readVarint32();
        if (HEX_DUMP) {
            console.log("varint32 size = " + size.toString(16));
        }
        return this.validate(((() => {
            var result = [];
            for (var i = 0; 0 < size ? i < size : i > size; 0 < size ? i++ : i++) {
                result.push(st_operation.fromByteBuffer(b));
            }
            return result;
        })()));
    },
    appendByteBuffer(b, object){
        if (!object) { object = []; }
        b.writeVarint32(object.length);
        var iterable = this.validate(object);
        for (var i = 0, o; i < iterable.length; i++) {
            o = iterable[i];
            st_operation.appendByteBuffer(b, o);
        }
        return;
    },
    fromObject(object){
        if (!object) { object = []; }
        return this.validate(((() => {
            var result = [];
            for (var i = 0, o; i < object.length; i++) {
                o = object[i];
                result.push(st_operation.fromObject(o));
            }
            return result;
        })()));
    },
    toObject(object, debug = {}){
        if (debug.use_default && object === undefined) {
            return [ st_operation.toObject(object, debug) ];
        }
        if (!object) { object = []; }
        return this.validate(((() => {
            var result = [];
            for (var i = 0, o; i < object.length; i++) {
                o = object[i];
                result.push(st_operation.toObject(o, debug));
            }
            return result;
        })()));
    }
    };
};

// global_parameters_update_operation current_fees
Types.fixed_array = function(count, st_operation) {
  return {
    fromByteBuffer: function(b) {
      var i, j, ref, results;
      results = [];
      for (i = j = 0, ref = count; j < ref; i = j += 1) {
        results.push(st_operation.fromByteBuffer(b));
      }
      return sortOperation(results, st_operation);
    },
    appendByteBuffer: function(b, object) {
      var i, j, ref;
      if (count !== 0) {
        v.required(object);
        object = sortOperation(object, st_operation)
      }
      for (i = j = 0, ref = count; j < ref; i = j += 1) {
        st_operation.appendByteBuffer(b, object[i]);
      }
    },
    fromObject: function(object) {
      var i, j, ref, results;
      if (count !== 0) {
        v.required(object);
      }
      results = [];
      for (i = j = 0, ref = count; j < ref; i = j += 1) {
        results.push(st_operation.fromObject(object[i]));
      }
      return results;
    },
    toObject: function(object, debug) {
      var i, j, k, ref, ref1, results, results1;
      if (debug == null) {
        debug = {};
      }
      if (debug.use_default && object === void 0) {
        results = [];
        for (i = j = 0, ref = count; j < ref; i = j += 1) {
          results.push(st_operation.toObject(void 0, debug));
        }
        return results;
      }
      if (count !== 0) {
        v.required(object);
      }
      results1 = [];
      for (i = k = 0, ref1 = count; k < ref1; i = k += 1) {
        results1.push(st_operation.toObject(object[i], debug));
      }
      return results1;
    }
  };
};

/* Supports instance numbers (11) or object types (1.2.11).  Object type
validation is enforced when an object type is used. */
var id_type = function(reserved_spaces, object_type){
    v.required(reserved_spaces, "reserved_spaces");
    v.required(object_type, "object_type");
    return {fromByteBuffer(b){
        return b.readVarint32();
    },
    appendByteBuffer(b, object){
        v.required(object);
        if (object.resolve !== undefined) { object = object.resolve; }
        // convert 1.2.n into just n
        if (/^[0-9]+\.[0-9]+\.[0-9]+$/.test(object)) {
            object = v.get_instance(reserved_spaces, object_type, object);
        }
        b.writeVarint32(v.to_number(object));
        return;
    },
    fromObject(object){
        v.required(object);
        if (object.resolve !== undefined) { object = object.resolve; }
        if (v.is_digits(object)) {
            return v.to_number(object);
        }
        return v.get_instance(reserved_spaces, object_type, object);
    },
    toObject(object, debug = {}){
        var object_type_id = chain_types.object_type[object_type];
        if (debug.use_default && object === undefined) {
            return `${reserved_spaces}.${object_type_id}.0`;
        }
        v.required(object);
        if (object.resolve !== undefined) { object = object.resolve; }
        if (/^[0-9]+\.[0-9]+\.[0-9]+$/.test(object)) {
            object = v.get_instance(reserved_spaces, object_type, object);
        }

        return `${reserved_spaces}.${object_type_id}.`+object;
    },
    };
};

Types.protocol_id_type = function(name){
    v.required(name, "name")
    return id_type(chain_types.reserved_spaces.protocol_ids, name);
};

Types.object_id_type =
    {fromByteBuffer(b){
        return ObjectId.fromByteBuffer(b);
    },
    appendByteBuffer(b, object){
        v.required(object);
        if (object.resolve !== undefined) { object = object.resolve; }
        object = ObjectId.fromString(object);
        object.appendByteBuffer(b);
        return;
    },
    fromObject(object){
        v.required(object);
        if (object.resolve !== undefined) { object = object.resolve; }
        return ObjectId.fromString(object);
    },
    toObject(object, debug = {}){
        if (debug.use_default && object === undefined) {
            return "0.0.0";
        }
        v.required(object);
        if (object.resolve !== undefined) {
            object = object.resolve;
        }
        object = ObjectId.fromString(object);
        return object.toString();
    }
    };

Types.vote_id =
    {TYPE: 0x000000FF,
    ID:   0xFFFFFF00,
    fromByteBuffer(b){
        var value = b.readUint32();
        return {
            type: value & this.TYPE,
            id: value & this.ID
        };
    },
    appendByteBuffer(b, object){
        v.required(object);
        if(object === "string")
            object = Types.vote_id.fromObject(object)

        var value = object.id << 8 | object.type
        b.writeUint32(value);
        return;
    },
    fromObject(object){
        v.required(object, "(type vote_id)");
        if(typeof object === "object") {
            v.required(object.type, "type")
            v.required(object.id, "id")
            return object
        }
        v.require_test(/^[0-9]+:[0-9]+$/, object, `vote_id format ${object}`);
        var [type, id] = object.split(':');
        v.require_range(0,0xff,type,`vote type ${object}`);
        v.require_range(0,0xffffff,id,`vote id ${object}`);
        return { type, id };
    },
    toObject(object, debug = {}){
        if (debug.use_default && object === undefined) {
            return "0:0";
        }
        v.required(object);
        if(typeof object === "string")
            object = Types.vote_id.fromObject(object)

        return object.type + ":" + object.id;
    },
    compare(a, b) {
        if(typeof a !== "object") a = Types.vote_id.fromObject(a)
        if(typeof b !== "object") b = Types.vote_id.fromObject(b)
        return parseInt(a.id) - parseInt(b.id);
    }
};

Types.optional = function(st_operation){
    v.required(st_operation, "st_operation");
    return {fromByteBuffer(b){
        if (!(b.readUint8() === 1)) {
            return undefined;
        }
        return st_operation.fromByteBuffer(b);
    },
    appendByteBuffer(b, object){
        if (object !== null && object !== undefined) {
            b.writeUint8(1);
            st_operation.appendByteBuffer(b, object);
        } else {
            b.writeUint8(0);
        }
        return;
    },
    fromObject(object){
        if (object === undefined) { return undefined; }
        return st_operation.fromObject(object);
    },
    toObject(object, debug = {}){
        // toObject is only null save if use_default is true
        var result_object = (() => {
            if (!debug.use_default && object === undefined) {
                return undefined;
            } else {
                return st_operation.toObject(object, debug);
            }
        })();

        if (debug.annotate) {
            if (typeof result_object === "object") {
                result_object.__optional = "parent is optional";
            } else {
                result_object = {__optional: result_object};
            }
        }
        return result_object;
    }
    };
};

Types.static_variant = function(_st_operations){
    return {
        nosort: true,
        st_operations: _st_operations,
    opTypeId(value) {
        let pos = 0, type_id
        if(typeof value === "number")
            type_id = value
        else {
            for(let op of this.st_operations) {
                if(op.operation_name === value) {
                    type_id = pos
                    break
                }
                pos++
            }
        }
        return type_id
    },
    fromByteBuffer(b){
        var type_id = b.readVarint32();
        var st_operation = this.st_operations[type_id];
        if (HEX_DUMP) {
            console.error(`static_variant id 0x${type_id.toString(16)} (${type_id})`);
        }
        v.required(st_operation, `operation ${type_id}`);
        return [
            type_id,
            st_operation.fromByteBuffer(b)
        ];
    },
    appendByteBuffer(b, object){
        v.required(object);
        var type_id = this.opTypeId(object[0]);
        var st_operation = this.st_operations[type_id];
        v.required(st_operation, `operation ${type_id}`);
        b.writeVarint32(type_id);
        st_operation.appendByteBuffer(b, object[1]);
        return;
    },
    fromObject(object){
        v.required(object);
        let type_id = this.opTypeId(object[0]);
        var st_operation = this.st_operations[type_id];
        v.required(st_operation, `operation ${type_id}`);
        return [
            type_id,
            st_operation.fromObject(object[1])
        ];
    },
    toObject(object, debug = {}){
        if (debug.use_default && object === undefined) {
            return [this.st_operations[0].operation_name, this.st_operations[0].toObject(undefined, debug)];
        }
        v.required(object);
        let type_id = this.opTypeId(object[0]);
        var st_operation = this.st_operations[type_id];
        v.required(st_operation, `operation ${type_id}`);
        return [
            st_operation.operation_name,
            st_operation.toObject(object[1], debug)
        ];
    },
    compare(a, b) {
        return strCmp(this.opTypeId(a[0]), this.opTypeId(b[0]))
    }
    };
};

Types.map = function(key_st_operation, value_st_operation){
    return {validate(array){
        if (!Array.isArray(array)) {
            throw new Error("expecting array");
        }
        var dup_map = {};
        for (var i = 0, o; i < array.length; i++) {
            o = array[i];
            var ref;
            if (!(o.length === 2)) {
                throw new Error("expecting two elements");
            }
            if (ref = typeof o[0], ['number', 'string'].indexOf(ref) >= 0) {
                if (dup_map[o[0]] !== undefined) {
                    throw new Error("duplicate (map)");
                }
                dup_map[o[0]] = true;
            }
        }
        return sortOperation(array, key_st_operation);
    },

    fromByteBuffer(b){
        var result = [];
        var end = b.readVarint32();
        for (var i = 0; 0 < end ? i < end : i > end; 0 < end ? i++ : i++) {
            result.push([
                key_st_operation.fromByteBuffer(b),
                value_st_operation.fromByteBuffer(b)
            ]);
        }
        return this.validate(result);
    },

    appendByteBuffer(b, object){
        this.validate(object);
        b.writeVarint32(object.length);
        for (var i = 0, o; i < object.length; i++) {
            o = object[i];
            key_st_operation.appendByteBuffer(b, o[0]);
            value_st_operation.appendByteBuffer(b, o[1]);
        }
        return;
    },
    fromObject(object){
        v.required(object);
        var result = [];
        for (var i = 0, o; i < object.length; i++) {
            o = object[i];
            result.push([
                key_st_operation.fromObject(o[0]),
                value_st_operation.fromObject(o[1])
            ]);
        }
        return this.validate(result)
    },
    toObject(object, debug = {}){
        if (debug.use_default && object === undefined) {
            return [
                [
                    key_st_operation.toObject(undefined, debug),
                    value_st_operation.toObject(undefined, debug)
                ]
            ];
        }
        v.required(object);
        object = this.validate(object);
        var result = []
        for (var i = 0, o; i < object.length; i++) {
            o = object[i];
            result.push([
                key_st_operation.toObject(o[0], debug),
                value_st_operation.toObject(o[1], debug)
            ]);
        }
        return result
    }
    };
};

Types.public_key = {
    toPublic(object){
        if (object.resolve !== undefined) { object = object.resolve; }
        return object == null ? object :
            object.Q ? object : PublicKey.fromStringOrThrow(object)
    },
    fromByteBuffer(b){
        return fp.public_key(b);
    },
    appendByteBuffer(b, object){
        v.required(object);
        fp.public_key(b, Types.public_key.toPublic(object));
        return;
    },
    fromObject(object){
        v.required(object);
        if (object.Q) { return object; }
        return Types.public_key.toPublic(object);
    },
    toObject(object, debug = {}){
        if (debug.use_default && object === undefined) {
            return ecc_config.address_prefix + "859gxfnXyUriMgUeThh1fWv3oqcpLFyHa3TfFYC4PK2HqhToVM";
        }
        v.required(object);
        return object.toString()
    },
    compare(a, b) {
        // sort decending
        return -1 * strCmp(a.toString(), b.toString())
    }
};

Types.address =
    {_to_address(object){
        v.required(object);
        if (object.addy) { return object; }
        return Address.fromString(object);
    },
    fromByteBuffer(b){
        return new Address(fp.ripemd160(b));
    },
    appendByteBuffer(b, object){
        fp.ripemd160(b, Types.address._to_address(object).toBuffer());
        return;
    },
    fromObject(object){
        return Types.address._to_address(object);
    },
    toObject(object, debug = {}){
        if (debug.use_default && object === undefined) {
            return ecc_config.address_prefix + "664KmHxSuQyDsfwo4WEJvWpzg1QKdg67S";
        }
        return Types.address._to_address(object).toString();
    },
    compare(a, b) {
        // sort decending
        return -1 * strCmp(a.toString(), b.toString())
    }
}

let strCmp = (a, b) => a > b ? 1 : a < b ? -1 : 0
let firstEl = el => Array.isArray(el) ? el[0] : el
let sortOperation = (array, st_operation) => {
    // console.log('operation.nosort', st_operation.nosort)
    return st_operation.nosort ? array :
    st_operation.compare ?
    array.sort((a,b)=> st_operation.compare(firstEl(a), firstEl(b))) : // custom compare operation
    array.sort((a,b)=>
        typeof firstEl(a) === "number" && typeof firstEl(b) === "number" ? firstEl(a) - firstEl(b) :
        // A binary string compare does not work. Performanance is very good so HEX is used..  localeCompare is another option.
        Buffer.isBuffer(firstEl(a)) && Buffer.isBuffer(firstEl(b)) ? strCmp(firstEl(a).toString("hex"), firstEl(b).toString("hex")) :
        strCmp(firstEl(a).toString(), firstEl(b).toString())
    )
}
