package com.ruoyi.common.utils;

import org.apache.commons.lang3.StringUtils;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.TimeZone;

/**
 * 普通时间处理工具类
 * <p>
 * 采用java8 里的jave.time包下类处理，该包下类方法时间线程安全的，适用于高并发环境 属于本工具类方法注释后标注*号
 * </p>
 * 时区转换时间处理工具类
 * <p>
 * 本工具类同时集成了关于时区转换的工具方法，适用于全球化时区处理
 * </p>
 *
 * @author zhouquan
 * @date 2021-03-04 14:35:00
 */
public class DateFormatUtil {
    /**
     * 格林威治时间0时区zoneId
     */
    private static final String UTC_ZoneId = "UTC";

    /**
     * 冬夏令时转换时间间隔
     */
    private static final long SW_HOURS = 1L;

    /**
     * 时间转换类
     */
    public static final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    /**
     * 时间转换类(yyyy-MM-dd)
     */
    public static final DateTimeFormatter dateFormatter1 = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    /**
     * 时间转换类(yyyy-MM)
     */
    public static final DateTimeFormatter dateFormatter2 = DateTimeFormatter.ofPattern("yyyy-MM");
    /**
     * 时间转换类(yyMMddHHmm)
     */
    public static final DateTimeFormatter dateFormatter3 = DateTimeFormatter.ofPattern("yyMMddHHmm");
    /**
     * 时间转换类(MMdd)
     */
    public static final DateTimeFormatter dateFormatter4 = DateTimeFormatter.ofPattern("MMdd");
    /**
     * 时间转换类(yyyyMMdd)
     */
    public static final DateTimeFormatter dateFormatter5 = DateTimeFormatter.ofPattern("yyyyMMdd");
    /**
     * 时间转换类(yyMMddHHmmssSSS)
     */
    public static final DateTimeFormatter dateFormatter6 = DateTimeFormatter.ofPattern("yyMMddHHmmssSSS");
    /**
     * 时间转换类(yyMMddHHmmss)
     */
    public static final DateTimeFormatter dateFormatter7 = DateTimeFormatter.ofPattern("yyMMddHHmmss");


    /**
     * 系统所在服务器的时区
     */
    private static String System_ZoneId;

    static {
        System_ZoneId = getSystemZoneId();
    }

    /**
     * 获取当前服务器时间
     * <p>
     * 因为采取的是LocalDateTime，所以是线程安全的，本方法获取时间会随着服务器夏/冬令时时间自动进行调整，需要注意
     * </p>
     *
     * @return 服务器时间(与服务器时间一致)
     */
    public static LocalDateTime getNow() {
        return LocalDateTime.now();
    }

    /**
     * 获取当前服务器时间（冬令时）===
     * <p>
     * 因为采取的是LocalDateTime，所以是线程安全的，本方法获取时间会随着服务器夏/冬令时时间自动进行调整，需要注意
     * </p>
     *
     * @return 服务器时间(与服务器时间一致)
     */
    public static LocalDateTime getNowIgnoreSummer() {
        return LocalDateTime.now(getZoneId(System_ZoneId));
    }

    /**
     * 获取当前服务器时间
     * <p>
     * 因为采取的是LocalDateTime，所以是线程安全的，本方法获取时间会随着服务器夏/冬令时时间自动进行调整，需要注意
     * </p>
     *
     * @return 服务器时间字符串(与服务器时间一致)
     */
    public static String getNow(DateTimeFormatter formatter) {
        return dateSwitchTimeString(LocalDateTime.now(), formatter);
    }

    /**
     * 获取当前服务器时间
     * <p>
     * 因为采取的是LocalDateTime，所以是线程安全的，本方法获取时间会随着服务器夏/冬令时时间自动进行调整，需要注意
     * </p>
     *
     * @return 服务器时间字符串(与服务器时间一致)
     */
    public static String getNowIgnoreSummer(DateTimeFormatter formatter) {
        return dateSwitchTimeString(LocalDateTime.now(getZoneId(System_ZoneId)), formatter);
    }

    /**
     * 字符串时间转换为时间对象（）
     *
     * @return 时间对象
     */
    public static LocalDateTime timeStringSwitchDate(String time, DateTimeFormatter formatter) {
        return LocalDateTime.parse(time, formatter);
    }

    /**
     * 时间对象转换为字符串时间
     *
     * @return 字符串时间
     */
    public static String dateSwitchTimeString(LocalDateTime time, DateTimeFormatter formatter) {
        return time.format(formatter);
    }

    /**
     * 获取当前服务器的zoneId 所在时区===
     *
     * @return 标准格式标识(UTC+08:00)
     */
    public static String getSystemZoneId() {
        TimeZone timeZone = TimeZone.getDefault();
        if (timeZone.getID().equals(UTC_ZoneId)) {
            return UTC_ZoneId;
        }
        // 获取当前服务器时区偏移量
        int offset = timeZone.getRawOffset();
        // 根据偏移量计算转换成标准格式
        return translateUTCByOffset(offset);
    }

    /**
     * 根据时间偏移量计算UTC格式
     *
     * @param offset
     *            时间偏移量
     * @return 标准格式标识(UTC+08:00)
     */
    private static String translateUTCByOffset(int offset) {
        StringBuffer sb = new StringBuffer(UTC_ZoneId);
        // 判断偏移量正负确定所在时区范围,冬x区+,西x区-
        sb.append(offset >= 0 ? "+" : "-");
        // 偏移量取绝对值，计算偏移时间
        offset = Math.abs(offset);
        int hour = offset / (1000 * 60 * 60);
        int minute = offset % (1000 * 60 * 60) / (1000 * 60);
        // 时间默认两位数，不足补0
        sb.append(strNumber(hour, 2) + ":" + strNumber(minute, 2));
        return sb.toString();
    }

    /**
     * 输出指定长度的字符串格式的数字（主要是输出类似于“0001”的数据）
     */
    private static String strNumber(int number, int length) {
        StringBuilder str = new StringBuilder(String.valueOf(number));
        int strLength = str.length();
        if (strLength >= length) {
            return str.toString();
        }
        for (int i = 0; i < length - strLength; i++) {
            str.insert(0, "0");
        }

        return str.toString();
    }

    /**
     * 获取时区id
     *
     * @param zoneId
     *            UTC+08:00
     * @return 返回时区id对象
     */
    public static ZoneId getZoneId(String zoneId) {
        return ZoneId.of(zoneId);
    }

    /**
     * 获取偏移量
     *
     * @param zoneId
     *            UTC+08:00
     * @return 返回时区id对象
     */
    public static ZoneOffset getZoneOffset(String zoneId) {
        if (zoneId.contains(UTC_ZoneId)) {
            zoneId = StringUtils.substringAfter(zoneId, UTC_ZoneId);
        }
        return ZoneOffset.of(zoneId);
    }

    /**
     * 获取时间转换类
     *
     * @param formatter
     *            转换格式
     * @return 返回获取时间转换对象
     */
    public static DateTimeFormatter getFormatter(String formatter) {
        return DateTimeFormatter.ofPattern(formatter);
    }

    /**
     * 获取时间毫秒值===
     * <p>
     * 本方法返回值如果初始时间是夏令时时间，那么返回的毫秒值会比System.currentTimeMillis()多1个小时
     * </p>
     *
     * @param sourceTime
     *            初始时间对象
     * @param sourceZoneId
     *            初始时间时区id
     * @return 毫秒数
     */
    public static long getMilli(LocalDateTime sourceTime, String sourceZoneId) {
        return sourceTime.toInstant(DateFormatUtil.getZoneOffset(sourceZoneId)).toEpochMilli();
    }

    /**
     * 获取时间毫秒值===
     * <p>
     * 本方法返回值如果初始时间是夏令时时间，那么返回的毫秒值会比System.currentTimeMillis()多1个小时
     * </p>
     *
     * @param sourceTimeString
     *            初始时间字符串
     * @param sourceZoneId
     *            初始时间时区id
     * @return 毫秒数
     */
    public static long getMilli(String sourceTimeString, String sourceZoneId, DateTimeFormatter formatter) {
        LocalDateTime date = DateFormatUtil.timeStringSwitchDate(sourceTimeString, formatter);
        return date.toInstant(DateFormatUtil.getZoneOffset(sourceZoneId)).toEpochMilli();
    }

    /**
     * 获取当前服务器时间(转换为指定时区)
     * <p>
     * 根据服务器当前所在时区，将本地时间转换为对应时区的时间， 本方法获取时间会随着服务器夏/冬令时时间自动进行调整
     * </p>
     *
     * @param targetZoneId
     *            目标时间时区标识 (UTC)
     * @return 转换为指定时区后的当前服务器时间
     */
    public static LocalDateTime getTargetZoneNow(String targetZoneId) {
        return LocalDateTime.now().atZone(getZoneId(System_ZoneId)).withZoneSameInstant(getZoneId(targetZoneId))
            .toLocalDateTime();
    }

    /**
     * 获取当前服务器时间(转换为指定时区)
     * <p>
     * 根据服务器当前所在时区，将本地时间转换为对应时区的时间， 本方法时间默认按冬令时规则 只要获取初始时间的时候，指定对应时区，获得的时间默认是按冬令时获得的，即使服务器当前时间是夏令时
     * </p>
     *
     * @param targetZoneId
     *            目标时间时区标识 (UTC)
     * @return 转换为指定时区后的当前服务器时间
     */
    public static LocalDateTime getTargetZoneNowIgnoreSummber(String targetZoneId) {
        return LocalDateTime.now(getZoneId(System_ZoneId)).atZone(getZoneId(System_ZoneId))
            .withZoneSameInstant(getZoneId(targetZoneId)).toLocalDateTime();
    }

    /**
     * 获取目标时间(转换为指定时区)
     * <p>
     * 目标时间是冬令时，则转换后指定时区的时间为冬令时；目标时间是夏令时，则转换后指定时区的时间为夏令时
     * </p>
     *
     * @param sourceTime
     *            初始时间
     * @param sourceZoneId
     *            初始时间所在时区
     * @param targetZoneId
     *            目标时间所在时区
     * @return 转换为指定时区后的目标时间时间
     */
    public static LocalDateTime switchTimeToTargetZone(LocalDateTime sourceTime, String sourceZoneId,
        String targetZoneId) {
        return sourceTime.atZone(getZoneId(sourceZoneId)).withZoneSameInstant(getZoneId(targetZoneId))
            .toLocalDateTime();
    }

    /**
     * 获取当前服务器时间(转换为指定时区)
     * <p>
     * 根据服务器当前所在时区，将本地时间转换为对应时区的时间， 本方法获取时间会随着服务器夏/冬令时时间自动进行调整
     * </p>
     *
     * @param targetZoneId
     *            目标时间时区标识 (UTC)
     * @return 转换为指定时区后的当前服务器时间(字符串)
     */
    public static String getTargetZoneNow(String targetZoneId, DateTimeFormatter formatter) {
        LocalDateTime localDateTime = LocalDateTime.now().atZone(getZoneId(System_ZoneId))
            .withZoneSameInstant(getZoneId(targetZoneId)).toLocalDateTime();
        return dateSwitchTimeString(localDateTime, formatter);
    }

    /**
     * 获取当前服务器时间(转换为指定时区)
     * <p>
     * 根据服务器当前所在时区，将本地时间转换为对应时区的时间， 本方法时间默认按冬令时规则 只要获取初始时间的时候，指定对应时区，获得的时间默认是按冬令时获得的，即使服务器当前时间是夏令时
     * </p>
     *
     * @param targetZoneId
     *            目标时间时区标识 (UTC)
     * @return 转换为指定时区后的当前服务器时间
     */
    public static String getTargetZoneNowIgnoreSummber(String targetZoneId, DateTimeFormatter formatter) {
        LocalDateTime localDateTime = LocalDateTime.now(getZoneId(System_ZoneId)).atZone(getZoneId(System_ZoneId))
            .withZoneSameInstant(getZoneId(targetZoneId)).toLocalDateTime();
        return dateSwitchTimeString(localDateTime, formatter);
    }

    /**
     * 获取目标时间(转换为指定时区)===
     * <p>
     * 目标时间是冬令时，则转换后指定时区的时间为冬令时；目标时间是夏令时，则转换后指定时区的时间为夏令时 本方法初始时间参数是时间对象
     * </p>
     *
     * @param sourceTime
     *            初始时间
     * @param sourceZoneId
     *            初始时间所在时区 (UTC+|-XX:XX)
     * @param targetZoneId
     *            目标时间所在时区 (UTC+|-XX:XX)
     * @return 转换为指定时区后的目标时间时间
     */
    public static String switchTimeToTargetZone(LocalDateTime sourceTime, DateTimeFormatter formatter,
        String sourceZoneId, String targetZoneId) {
        LocalDateTime localDateTime =
            sourceTime.atZone(getZoneId(sourceZoneId)).withZoneSameInstant(getZoneId(targetZoneId)).toLocalDateTime();
        return dateSwitchTimeString(localDateTime, formatter);
    }

    /**
     * 获取目标时间(转换为指定时区)
     * <p>
     * 目标时间是冬令时，则转换后指定时区的时间为冬令时；目标时间是夏令时，则转换后指定时区的时间为夏令时 本方法初始时间参数是字符串形式
     * </p>
     *
     * @param sourceTimeString
     *            初始时间字符串
     * @param sourceZoneId
     *            初始时间所在时区 (UTC+|-XX:XX)
     * @param targetZoneId
     *            目标时间所在时区 (UTC+|-XX:XX)
     * @return 转换为指定时区后的目标时间时间
     */
    public static String switchTimeToTargetZone(String sourceTimeString, DateTimeFormatter formatter,
        String sourceZoneId, String targetZoneId) {
        LocalDateTime sourceTime = timeStringSwitchDate(sourceTimeString, formatter);
        LocalDateTime localDateTime =
            sourceTime.atZone(getZoneId(sourceZoneId)).withZoneSameInstant(getZoneId(targetZoneId)).toLocalDateTime();
        return dateSwitchTimeString(localDateTime, formatter);
    }

    /**
     * 冬夏令时转换
     * <p>
     * 目标时间是冬令时，则转换后指定时区的时间为冬令时；目标时间是夏令时，则转换后指定时区的时间为夏令时
     * </p>
     *
     * @param sourceTime
     *            待转换时间
     * @param type
     *            1 冬转夏 -1 夏转冬
     * @return 转换后时间
     */
    public static LocalDateTime switchBetweenSW(LocalDateTime sourceTime, int type) {
        if (1 == type) {
            // 冬令时转夏令时 +1小时
            return sourceTime.plusHours(SW_HOURS);
        } else if (-1 == type) {
            // 夏令时转冬令时 -1小时
            return sourceTime.minusHours(SW_HOURS);
        } else {
            return sourceTime;
        }
    }

    /**
     * 冬夏令时转换
     * <p>
     * 目标时间是冬令时，则转换后指定时区的时间为冬令时；目标时间是夏令时，则转换后指定时区的时间为夏令时
     * </p>
     *
     * @param sourceTimeString
     *            待转换时间字符串
     * @param type
     *            1 冬转夏 0 夏转冬
     * @return 转换后时间
     */
    public static LocalDateTime switchBetweenSW(String sourceTimeString, int type, DateTimeFormatter formatter) {
        LocalDateTime sourceTime = DateFormatUtil.timeStringSwitchDate(sourceTimeString, formatter);
        if (1 == type) {
            // 冬令时转夏令时 +1小时
            return sourceTime.plusHours(SW_HOURS);
        } else if (-1 == type) {
            // 夏令时转冬令时 -1小时
            return sourceTime.minusHours(SW_HOURS);
        } else {
            return sourceTime;
        }
    }

    /**
     * 时间戳转换成当前时间字符串
     *
     * @param timestamp
     *            时间戳
     * @param sourceZoneId
     *            目标时间时区
     * @param formatter
     *            格式转换类
     * @return 时间字符串
     */
    public static String timestampSwitchToTime(Long timestamp, String sourceZoneId, DateTimeFormatter formatter) {
        LocalDateTime date = Instant.ofEpochMilli(timestamp).atZone(getZoneOffset(sourceZoneId)).toLocalDateTime();
        return dateSwitchTimeString(date, formatter);
    }

    /**
     * 时间戳转换成当前时间对象
     *
     * @param timestamp
     *            时间戳
     * @param sourceZoneId
     *            目标时间时区
     * @return 时间对象
     */
    public static LocalDateTime timestampSwitchToTime(Long timestamp, String sourceZoneId) {
        return Instant.ofEpochMilli(timestamp).atZone(getZoneOffset(sourceZoneId)).toLocalDateTime();
    }

}
