# Copyright 2016-2018 The NATS Authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
require 'securerandom'

module NATS
  class NUID
    DIGITS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('')
    BASE          = 62
    PREFIX_LENGTH = 12
    SEQ_LENGTH    = 10
    TOTAL_LENGTH  = PREFIX_LENGTH + SEQ_LENGTH
    MAX_SEQ       = BASE**10
    MIN_INC       = 33
    MAX_INC       = 333
    INC = MAX_INC - MIN_INC

    def initialize
      @prand    = Random.new
      @seq      = @prand.rand(MAX_SEQ)
      @inc      = MIN_INC + @prand.rand(INC)
      @prefix   = ''
      randomize_prefix!
    end

    def next
      @seq += @inc
      if @seq >= MAX_SEQ
        randomize_prefix!
        reset_sequential!
      end
      l = @seq

      # Do this inline 10 times to avoid even more extra allocs,
      # then use string interpolation of everything which works
      # faster for doing concat.
      s_10 = DIGITS[l % BASE];

      # Ugly, but parallel assignment is slightly faster here...
      s_09, s_08, s_07, s_06, s_05, s_04, s_03, s_02, s_01 = \
      (l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE]),\
      (l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE]),\
      (l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE]), (l /= BASE; DIGITS[l % BASE])
      "#{@prefix}#{s_01}#{s_02}#{s_03}#{s_04}#{s_05}#{s_06}#{s_07}#{s_08}#{s_09}#{s_10}"
    end

    def randomize_prefix!
      @prefix = \
      SecureRandom.random_bytes(PREFIX_LENGTH).each_byte
        .reduce('') do |prefix, n|
        prefix << DIGITS[n % BASE]
      end
    end

    private

    def reset_sequential!
      @seq = @prand.rand(MAX_SEQ)
      @inc = MIN_INC + @prand.rand(INC)
    end

    class << self
      @@nuid = NUID.new.extend(MonitorMixin)
      def next
        @@nuid.synchronize do
          @@nuid.next
        end
      end
    end
  end
end
