sourceafBson::ObjectId.fan

using inet
using concurrent

** (BSON Type) - 
** A globally unique identifier for MongoDB objects.
**
** Consists of 12 bytes, divided as follows:
** 
** pre>
** | 0  1  2  3 | 4  5  6 | 7  8 | 9 10 11
** | timestamp  | machine | pid  | inc
** <pre
** 
** @See
**  - `http://docs.mongodb.org/manual/reference/object-id/`
**  - `http://api.mongodb.org/java/2.12/org/bson/types/ObjectId.html`
@Serializable { simple = true }
const class ObjectId {
    private static const AtomicInt  counter     := AtomicInt(0)
    // TODO: this machine hash isn't very good on IP6 addresses
    private static const Int        thisMachine := IpAddr.local.bytes.readS4
    // one cannot get the ProcessId in Java - http://fantom.org/sidewalk/topic/856
    // Even the Java impl of ObjectId generates a random Int 
    private static const Int        thisPid     := Int.random
    
    ** The creation timestamp with a 1 second accuracy.
    const DateTime  timestamp
    
    ** A 4-byte machine identifier, usually the IP address.
    const Int       machine
    
    ** A 2-byte process id that this instance was created under.
    const Int       pid
    
    ** A 3-byte 'inc' value.
    const Int       inc
    
    @NoDoc
    override const Int hash 
  
    ** Creates a new 'ObjectId'.
    new make() : this.makeAll(DateTime.now, thisMachine, thisPid, counter.incrementAndGet) { }
    
    ** Useful for testing.
    @NoDoc
    new makeAll(DateTime timestamp, Int machine, Int pid, Int inc) {
        this.timestamp  = timestamp.floor(1sec)
        this.machine    = machine.and(0xFFFFFF)
        this.pid        = pid.and(0xFFFF)
        this.inc        = inc.and(0xFFFFFF)
        this.hash       = [this.timestamp.toJava, this.machine, this.pid, this.inc].reduce(42) |Int result, val -> Int| {
            return (37 * result) + val
        }
    }

    ** Create an 'ObjectId' from a hex string.
    static new fromStr(Str hex, Bool checked := true) {
        if (hex.size != 24 || !hex.all { it.isAlphaNum })
            return null ?: (checked ? throw ParseErr("Could not parse ObjectId: ${hex}") : null)

        try {
            timeFromStr := hex[ 0..< 8].toInt(16)
            machine     := hex[ 8..<14].toInt(16)
            pid         := hex[14..<18].toInt(16)
            inc         := hex[18..<24].toInt(16)
            timeInSecs  := Buf(4).writeI4(timeFromStr).flip.readS4  // re-read as a signed number
            timestamp   := Utils.fromUnixEpoch(timeInSecs * 1000)
            return ObjectId(timestamp, machine, pid, inc)

        } catch (Err e) {
            return null ?: (checked ? throw ParseErr("Could not parse ObjectId: ${hex}", e) : null)
        }
    }

    ** Reads an 'ObjectId' from the given stream.
    ** 
    ** Note the stream is **not** closed.
    static new fromStream(InStream in) {
        origEndian  := in.endian
        in.endian   = Endian.big
        timestamp   := Utils.fromUnixEpoch(in.readS4 * 1000)
        machine     := in.readBufFully(null, 3).toHex.toInt(16)
        pid         := in.readU2
        inc         := in.readBufFully(null, 3).toHex.toInt(16)
        in.endian   = origEndian
        return ObjectId(timestamp, machine, pid, inc)
    }

    ** Converts this instance into a 24 character hexadecimal string representation.
    Str toHex() {
        toBuf.toHex
    }

    ** Encodes this 'ObjectId' into an 12 byte buffer.
    ** The returned buffer is positioned at the start and is ready to read.
    Buf toBuf() {
        buf := Buf(12)
        writeToBuf(buf, timestamp.toJava / 1000, 4)
        writeToBuf(buf, machine, 3)
        writeToBuf(buf, pid, 2)
        writeToBuf(buf, inc, 3)
        return buf.flip
    }

    private static Void writeToBuf(Buf buf, Int val, Int noOfBytes) {
        noOfBytes.times |i| {
            buf.write(val.shiftr(8 * (noOfBytes - i - 1)).and(0xFF))
        }
    }

    ** Returns this 'ObjectId' as a 24 character hexadecimal string.
    override Str toStr() {
        toHex
    }
  
    @NoDoc
    override Bool equals(Obj? obj) {
        objId := obj as ObjectId
        if (objId       == null)            return false
        if (inc         != objId.inc)       return false
        if (pid         != objId.pid)       return false
        if (machine     != objId.machine)   return false
        if (timestamp   != objId.timestamp) return false
        return true
    }
}