/*
 * Copyright 1999-2018 Alibaba Group Holding Ltd.
 *
 * 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.
 */

package com.alibaba.nacos.core.distributed.id;

import com.alibaba.nacos.consistency.IdGenerator;
import com.alibaba.nacos.sys.env.EnvUtil;
import com.alibaba.nacos.sys.utils.InetUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * copy from http://www.cluozy.com/home/hexo/2018/08/11/shariding-JDBC-snowflake/.
 *
 * <strong>WorkerId</strong> generation policy: Calculate the InetAddress hashcode
 *
 * <p>The repeat rate of the dataCenterId, the value of the maximum dataCenterId times the time of each Raft election.
 * The
 * time for raft to select the master is generally measured in seconds. If the interval of an election is 5 seconds, it
 * will take 150 seconds for the DataCenterId to be repeated. This is still based on the situation that the new master
 * needs to be selected after each election of the Leader
 *
 * @author <a href="mailto:liaochunyhm@live.com">liaochuntao</a>
 */
@SuppressWarnings("all")
public class SnowFlowerIdGenerator implements IdGenerator {
    
    private static final String DATETIME_PATTERN =  "yyyy-MM-dd HH:mm:ss.SSS";
    
    /**
     * Start time intercept (2018-08-05 08:34)
     */
    public static final long EPOCH = 1533429240000L;
    
    private static final Logger logger = LoggerFactory.getLogger(SnowFlowerIdGenerator.class);
    
    // the bits of sequence
    private static final long SEQUENCE_BITS = 12L;
    
    // the bits of workerId
    private static final long WORKER_ID_BITS = 10L;
    
    // the mask of sequence (111111111111B = 4095)
    private static final long SEQUENCE_MASK = 4095L;
    
    // the left shift bits of workerId equals 12 bits
    private static final long WORKER_ID_LEFT_SHIFT_BITS = 12L;
    
    // the left shift bits of timestamp equals 22 bits (WORKER_ID_LEFT_SHIFT_BITS + workerId)
    private static final long TIMESTAMP_LEFT_SHIFT_BITS = 22L;
    
    // the max of worker ID is 1024
    private static final long WORKER_ID_MAX_VALUE = 1024L;
    
    //CLOCK_REALTIME
    private final long startWallTime = System.currentTimeMillis();
    
    //CLOCK_MONOTONIC
    private final long monotonicStartTime = System.nanoTime();
    
    private long workerId;
    
    private long sequence;
    
    private long lastTime;
    
    private long currentId;
    
    {
        long workerId = EnvUtil.getProperty("nacos.core.snowflake.worker-id", Integer.class, -1);
        
        if (workerId != -1) {
            this.workerId = workerId;
        } else {
            InetAddress address;
            try {
                address = InetAddress.getByName(InetUtils.getSelfIP());
            } catch (final UnknownHostException e) {
                throw new IllegalStateException("Cannot get LocalHost InetAddress, please check your network!", e);
            }
            byte[] ipAddressByteArray = address.getAddress();
            this.workerId = (((ipAddressByteArray[ipAddressByteArray.length - 2] & 0B11) << Byte.SIZE) + (
                    ipAddressByteArray[ipAddressByteArray.length - 1] & 0xFF));
        }
    }
    
    @Override
    public void init() {
        initialize(workerId);
    }
    
    @Override
    public long currentId() {
        return currentId;
    }
    
    @Override
    public long workerId() {
        return workerId;
    }
    
    @Override
    public synchronized long nextId() {
        long currentMillis = currentTimeMillis();
        if (this.lastTime == currentMillis) {
            if (0L == (this.sequence = ++this.sequence & SEQUENCE_MASK)) {
                currentMillis = this.waitUntilNextTime(currentMillis);
            }
        } else {
            this.sequence = 0L;
        }
        
        this.lastTime = currentMillis;
        
        if (logger.isDebugEnabled()) {
            logger.debug("{}-{}-{}", (new SimpleDateFormat(DATETIME_PATTERN)).format(new Date(this.lastTime)),
                    workerId, this.sequence);
        }
        
        currentId = currentMillis - EPOCH << 22 | workerId << 12 | this.sequence;
        return currentId;
    }
    
    @Override
    public Map<Object, Object> info() {
        Map<Object, Object> info = new HashMap<>(4);
        info.put("currentId", currentId);
        info.put("workerId", workerId);
        return info;
    }
    
    // ==============================Constructors=====================================
    
    /**
     * init
     *
     * @param workerId worker id (0~1024)
     */
    public void initialize(long workerId) {
        if (workerId > WORKER_ID_MAX_VALUE || workerId < 0) {
            throw new IllegalArgumentException(
                    String.format("worker Id can't be greater than %d or less than 0, current workId %d",
                            WORKER_ID_MAX_VALUE, workerId));
        }
        this.workerId = workerId;
    }
    
    /**
     * Block to the next millisecond until a new timestamp is obtained
     *
     * @param lastTimestamp The time intercept of the last ID generated
     * @return Current timestamp
     */
    private long waitUntilNextTime(long lastTimestamp) {
        long time;
        time = currentTimeMillis();
        while (time <= lastTimestamp) {
            time = currentTimeMillis();
        }
        
        return time;
    }
    
    private long currentTimeMillis() {
        return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - monotonicStartTime) + startWallTime;
    }
    
}
