/*
 * Copyright (c) 2017 Baidu, Inc. All Rights Reserve. 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.crunii.micro.ds.uid.worker.impl;

import com.crunii.micro.common.constants.DSConstants;
import com.crunii.micro.domain.common.jdbc.BaseNPJdbcTemplate;
import com.crunii.micro.ds.uid.domain.TWorkerNode;
import com.crunii.micro.ds.uid.util.DateUtils;
import com.crunii.micro.ds.uid.util.DockerUtils;
import com.crunii.micro.ds.uid.util.NetUtils;
import com.crunii.micro.ds.uid.worker.BitsAllocator;
import com.crunii.micro.ds.uid.worker.UidGenerator;
import com.crunii.micro.ds.uid.worker.WorkerNodeType;
import com.crunii.micro.ds.uid.worker.exception.UidGenerateException;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;

import javax.annotation.Resource;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
 * Represents an implementation of {@link UidGenerator} The unique id has 64bits (long), default allocated as blow:<br>
 * <li>sign: The highest bit is 0
 * <li>delta seconds: The next 28 bits, represents delta seconds since a customer epoch(2016-05-20 00:00:00.000). Supports about 8.7 years until to 2024-11-20 21:24:16
 * <li>worker id: The next 22 bits, represents the worker's id which assigns based on database, max id is about 420W
 * <li>sequence: The next 13 bits, represents a sequence within the same second, max for 8192/s<br>
 * <br>
 * The {@link DefaultUidGenerator#parseUID(long)} is a tool method to parse the bits <pre>{@code
 * +------+----------------------+----------------+-----------+
 * | sign |     delta seconds    | worker node id | sequence  |
 * +------+----------------------+----------------+-----------+
 *   1bit          28bits              22bits         13bits
 * }</pre> You can also specified the bits by Spring property setting.
 * <li>timeBits: default as 28
 * <li>workerBits: default as 22
 * <li>seqBits: default as 13
 * <li>epochStr: Epoch date string format 'yyyy-MM-dd'. Default as '2016-05-20'
 * <p>
 * <b>Note that:</b> The total bits must be 64 -1
 *
 * @author yutianbao
 */
public class DefaultUidGenerator implements UidGenerator, InitializingBean {
	private static final Logger LOGGER = LoggerFactory.getLogger(DefaultUidGenerator.class);

	/**
	 * Bits allocate
	 */
	protected int timeBits = 28;
	protected int workerBits = 22;
	protected int seqBits = 13;

	/**
	 * Customer epoch, unit as second. For example 2016-05-20 (ms: 1463673600000)
	 */
	protected String epochStr = "2023-02-20";
	protected long epochSeconds = TimeUnit.MILLISECONDS.toSeconds(1463673600000L);

	/**
	 * Stable fields after spring bean initializing
	 */
	protected BitsAllocator bitsAllocator;
	protected long workerId;

	/**
	 * Volatile fields caused by nextId()
	 */
	protected long sequence = 0L;
	protected long lastSecond = -1L;

	@Resource(name = "mainNPJT")
	protected BaseNPJdbcTemplate NPJT;

	@Override
	public void afterPropertiesSet() throws Exception {
		// initialize bits allocator
		bitsAllocator = new BitsAllocator(timeBits, workerBits, seqBits);

		TWorkerNode workerNode = new TWorkerNode();
		if (DockerUtils.isDocker()) {
			workerNode.setNodeType(WorkerNodeType.CONTAINER.value());
			workerNode.setHostName(DockerUtils.getDockerHost());
			workerNode.setPort(DockerUtils.getDockerPort());
		} else {
			workerNode.setNodeType(WorkerNodeType.ACTUAL.value());
			workerNode.setHostName(NetUtils.getLocalAddress());
			workerNode.setPort(System.currentTimeMillis() + "-" + RandomUtils.nextInt(0, 100000));
		}
		Date date = new Date();
		workerNode.setLaunchDate(date);
		workerNode.setCreateTime(date);
		workerNode.setModifyTime(date);

		String sql = "insert into t_worker_node(host_name,port,node_type,launch_date,modify_time,create_time) values(:hostName,:port,:nodeType,:launchDate,:createTime,:modifyTime)";
		KeyHolder keyHolder = new GeneratedKeyHolder();
		SqlParameterSource paramSource = new BeanPropertySqlParameterSource(workerNode);
		NPJT.update(sql, paramSource, keyHolder);
		String dbType = NPJT.getDbType();
		//适配数据库
		if (dbType.equalsIgnoreCase(DSConstants.DATABASE_MYSQL)) {
			this.workerId = keyHolder.getKey().longValue();
		} else if (dbType.equalsIgnoreCase(DSConstants.DATABASE_POSTGRESQL)) {
			this.workerId = Long.valueOf(keyHolder.getKeyList().get(0).get("id").toString());
		} else {
			this.workerId = keyHolder.getKey().longValue();
		}

		if (workerId > bitsAllocator.getMaxWorkerId()) {
			throw new RuntimeException("Worker id " + workerId + " exceeds the max " + bitsAllocator.getMaxWorkerId());
		}
		LOGGER.info("Initialized bits(1, {}, {}, {}) for workerID:{}", timeBits, workerBits, seqBits, workerId);
	}

	@Override
	public long getUID() {
		try {
			return nextId();
		} catch (Exception e) {
			LOGGER.error("Generate unique id exception. ", e);
			throw new UidGenerateException(e);
		}
	}

	@Override
	public String parseUID(long uid) {
		long totalBits = BitsAllocator.TOTAL_BITS;
		long signBits = bitsAllocator.getSignBits();
		long timestampBits = bitsAllocator.getTimestampBits();
		long workerIdBits = bitsAllocator.getWorkerIdBits();
		long sequenceBits = bitsAllocator.getSequenceBits();

		// parse UID
		long sequence = (uid << (totalBits - sequenceBits)) >>> (totalBits - sequenceBits);
		long workerId = (uid << (timestampBits + signBits)) >>> (totalBits - workerIdBits);
		long deltaSeconds = uid >>> (workerIdBits + sequenceBits);

		Date thatTime = new Date(TimeUnit.SECONDS.toMillis(epochSeconds + deltaSeconds));
		String thatTimeStr = DateUtils.formatByDateTimePattern(thatTime);

		// format as string
		return String.format("{\"UID\":\"%d\",\"timestamp\":\"%s\",\"workerId\":\"%d\",\"sequence\":\"%d\"}",
				uid, thatTimeStr, workerId, sequence);
	}

	/**
	 * Get UID
	 *
	 * @return UID
	 * @throws UidGenerateException in the case: Clock moved backwards; Exceeds the max timestamp
	 */
	protected synchronized long nextId() {
		long currentSecond = getCurrentSecond();

		// Clock moved backwards, refuse to generate com.crunii.micro.ds.uid
		if (currentSecond < lastSecond) {
			long refusedSeconds = lastSecond - currentSecond;
			throw new UidGenerateException("Clock moved backwards. Refusing for %d seconds", refusedSeconds);
		}

		// At the same second, increase sequence
		if (currentSecond == lastSecond) {
			sequence = (sequence + 1) & bitsAllocator.getMaxSequence();
			// Exceed the max sequence, we wait the next second to generate com.crunii.micro.ds.uid
			if (sequence == 0) {
				currentSecond = getNextSecond(lastSecond);
			}

			// At the different second, sequence restart from zero
		} else {
			sequence = 0L;
		}

		lastSecond = currentSecond;

		// Allocate bits for UID
		return bitsAllocator.allocate(currentSecond - epochSeconds, workerId, sequence);
	}

	/**
	 * Get next millisecond
	 */
	private long getNextSecond(long lastTimestamp) {
		long timestamp = getCurrentSecond();
		while (timestamp <= lastTimestamp) {
			timestamp = getCurrentSecond();
		}

		return timestamp;
	}

	/**
	 * Get current second
	 */
	private long getCurrentSecond() {
		long currentSecond = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis());
		if (currentSecond - epochSeconds > bitsAllocator.getMaxDeltaSeconds()) {
			throw new UidGenerateException("Timestamp bits is exhausted. Refusing UID generate. Now: " + currentSecond);
		}

		return currentSecond;
	}

	public void setTimeBits(int timeBits) {
		if (timeBits > 0) {
			this.timeBits = timeBits;
		}
	}

	public void setWorkerBits(int workerBits) {
		if (workerBits > 0) {
			this.workerBits = workerBits;
		}
	}

	public void setSeqBits(int seqBits) {
		if (seqBits > 0) {
			this.seqBits = seqBits;
		}
	}

	public void setEpochStr(String epochStr) {
		if (StringUtils.isNotBlank(epochStr)) {
			this.epochStr = epochStr;
			this.epochSeconds = TimeUnit.MILLISECONDS.toSeconds(DateUtils.parseByDayPattern(epochStr).getTime());
		}
	}
}
