/*
 * Copyright [2020-2030] [https://www.stylefeng.cn]
 *
 * 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.
 *
 * Guns采用APACHE LICENSE 2.0开源协议，您在使用过程中，需要注意以下几点：
 *
 * 1.请不要删除和修改根目录下的LICENSE文件。
 * 2.请不要删除和修改Guns源码头部的版权声明。
 * 3.请保留源码和相关描述文件的项目出处，作者声明等。
 * 4.分发源码时候，请注明软件出处 https://gitee.com/stylefeng/guns
 * 5.在修改包名，模块名称，项目代码等时，请注明软件出处 https://gitee.com/stylefeng/guns
 * 6.若您的项目无法满足以上几点，可申请商业授权
 */
package cn.stylefeng.roses.kernel.log.file;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import cn.stylefeng.roses.kernel.log.api.LogRecordApi;
import cn.stylefeng.roses.kernel.log.api.constants.LogFileConstants;
import cn.stylefeng.roses.kernel.log.api.pojo.record.LogRecordDTO;
import cn.stylefeng.roses.kernel.log.api.threadpool.LogManagerThreadPool;
import com.alibaba.fastjson.JSON;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import static cn.stylefeng.roses.kernel.log.api.constants.LogFileConstants.FILE_CONTRACT_SYMBOL;
import static cn.stylefeng.roses.kernel.log.api.constants.LogFileConstants.FILE_SUFFIX;

/**
 * 文件存储方式的日志记录器
 *
 * @author fengshuonan
 * @since 2020/10/28 14:52
 */
public class FileLogRecordServiceImpl implements LogRecordApi {

    private final LogManagerThreadPool logManagerThreadPool;

    private final LogRefreshManager logRefreshManager;

    private final String fileSavePath;

    public FileLogRecordServiceImpl(String fileSavePath, LogManagerThreadPool logManagerThreadPool) {
        this.fileSavePath = fileSavePath;
        this.logManagerThreadPool = logManagerThreadPool;
        this.logRefreshManager = new LogRefreshManager();
        this.logRefreshManager.start();
    }

    public FileLogRecordServiceImpl(String fileSavePath, LogManagerThreadPool logManagerThreadPool, long sleepTime, int maxCount) {
        this.fileSavePath = fileSavePath;
        this.logManagerThreadPool = logManagerThreadPool;
        this.logRefreshManager = new LogRefreshManager(sleepTime, maxCount);
        this.logRefreshManager.start();
    }

    /**
     * 日志文件的组成形式应为appName-年-月-日.log
     *
     * @author fengshuonan
     * @since 2020/10/28 15:53
     */
    @Override
    public void add(LogRecordDTO logRecordDTO) {

        if (logRecordDTO == null) {
            return;
        }

        // 输出日志
        addBatch(CollectionUtil.list(false, logRecordDTO));
    }

    /**
     * 批量输出日志
     *
     * @param list 待输出日志列表
     * @author majianguo
     * @since 2020/11/2 下午2:59
     */
    @Override
    public void addBatch(List<LogRecordDTO> list) {

        if (ObjectUtil.isEmpty(list)) {
            return;
        }

        // 获取appName
        String appName = list.get(0).getAppName();
        if (StrUtil.isBlank(appName)) {
            appName = LogFileConstants.DEFAULT_LOG_FILE_NAME;
        }

        // 获取日志记录的日期
        Date dateTime = list.get(0).getDateTime();
        if (dateTime == null) {
            dateTime = new Date();
        }
        String dateStr = DateUtil.formatDate(dateTime);

        // 拼接文件名
        String fileName = appName + FILE_CONTRACT_SYMBOL + dateStr + FILE_SUFFIX;

        // 文件绝对路径生成，带文件名的完整路径
        String fileAbsolutePath = fileSavePath + File.separator + fileName;

        // 判断文件是否存在，不存在则创建
        boolean existFlag = FileUtil.exist(fileAbsolutePath);
        if (!existFlag) {
            FileUtil.touch(fileAbsolutePath);
        }

        // 将对象转换成JSON输出
        List<String> outList = new ArrayList<>();
        for (LogRecordDTO recordDTO : list) {
            outList.add(JSON.toJSONString(recordDTO));
        }

        // 追加日志内容
        FileUtil.appendLines(outList, fileAbsolutePath, StandardCharsets.UTF_8);
    }

    @Override
    public void addAsync(LogRecordDTO logRecordDTO) {
        logManagerThreadPool.executeLog(new TimerTask() {
            @Override
            public void run() {
                logRefreshManager.putLog(logRecordDTO);
            }
        });
    }

    /**
     * 日志刷新管理器
     * <p>
     * 该类暂存所有将要写出到磁盘的日志，使用内存缓冲区减少对磁盘IO的操作
     * <p>
     * 该类维护一个最大日志数和一个刷新日志间隔，满足任意一个条件即可触发从内存写出日志到磁盘的操作
     *
     * @author majianguo
     * @since 2020/10/31 15:05
     */
    class LogRefreshManager extends Thread {

        /**
         * Hutool日志对象
         */
        private final Log log = LogFactory.get();

        /**
         * 刷新日志间隔(默认3秒),单位毫秒
         */
        private final long sleepTime;

        /**
         * 满足多少条就强制刷新一次（默认300条）,该值只是一个大约值，实际记录并不会一定等于该值（可能会大于该值，不可能小于该值）
         */
        private final int maxCount;

        /**
         * 刷新数据时间标志,每次刷新都记录当前的时间戳，方便定时刷新准确判断上次刷新和本次刷新的时间间隔
         */
        private final AtomicLong refreshMark = new AtomicLong();

        public LogRefreshManager() {
            this.sleepTime = 3000;
            this.maxCount = 300;
        }

        public LogRefreshManager(long sleepTime) {
            this.sleepTime = sleepTime;
            this.maxCount = 300;
        }

        public LogRefreshManager(int maxCount) {
            this.sleepTime = 3000;
            this.maxCount = maxCount;
        }

        public LogRefreshManager(long sleepTime, int maxCount) {
            this.sleepTime = sleepTime;
            this.maxCount = maxCount;
        }

        /**
         * 未处理日志的消息队列
         */
        private final Queue<LogRecordDTO> queue = new ConcurrentLinkedQueue<>();

        /**
         * 消息总数,队列的size方法会遍历一遍队列，所以自己维护大小
         */
        public AtomicInteger count = new AtomicInteger(0);

        /**
         * 往队列内新增一条日志数据
         *
         * @param logRecordDTO 日志对象
         * @author majianguo
         * @since 2020/10/31 14:59
         */
        public void putLog(LogRecordDTO logRecordDTO) {

            int queueDataCount = count.get();

            // 如果是第一条消息，刷新一次refreshMark
            if (queueDataCount == 0) {
                refreshMark.getAndSet(System.currentTimeMillis());
            }

            // 如果后续写入磁盘的操作有异常(磁盘满了)，为了防止日志刷不出去导致OOM,内存中最大只保留maxCount数一倍的日志，后续日志将丢弃
            if (queueDataCount >= (maxCount * 2)) {
                return;
            }

            queue.offer(logRecordDTO);
            count.incrementAndGet();
        }

        /**
         * 刷新日志到磁盘的操作
         *
         * @author majianguo
         * @since 2020/10/31 15:48
         */
        private void refresh() {
            // 让睡眠线程本次不要再调本方法，睡眠至下次看refreshMark的值再决定要不要调用本方法
            refreshMark.getAndSet(System.currentTimeMillis());

            // 获取总数
            int num = count.getAndSet(0);

            // 缓冲队列中所有的数据
            List<LogRecordDTO> cacheAll = new ArrayList<>(num);

            try {
                // 在队列中读取num条数据，放入list
                for (int i = 0; i < num; i++) {
                    LogRecordDTO item = queue.poll();
                    if (null == item) {
                        break;
                    }
                    cacheAll.add(item);
                }

                // 调用方法刷新到磁盘
                addBatch(cacheAll);

            } catch (Exception e) {

                // 有异常把日志刷回队列，不要丢掉(这里可能会导致日志顺序错乱)
                for (LogRecordDTO recordDTO : cacheAll) {
                    queue.offer(recordDTO);
                }

                // 打印日志
                if (log.isDebugEnabled()) {
                    e.printStackTrace();
                }
                log.error(e.getMessage());
            }

        }

        /**
         * 日志数据定时执行器
         * <p>
         * 用于定时检测日志数据是否可以写入数据
         *
         * @author majianguo
         * @since 2020/10/31 15:57
         */
        private void timing() {
            try {
                // 如果是激活状态，且消息数大于零，且符合上次调用refresh方法到目前时间的间隔，那就调用一次refresh方法
                if ((refreshMark.get() + sleepTime) <= System.currentTimeMillis() && count.get() > 0) {
                    refresh();
                }
            } catch (Exception e) {
                if (log.isDebugEnabled()) {
                    e.printStackTrace();
                }
                log.error(e.getMessage());
            }
        }

        /**
         * 日志数据监听器
         * <p>
         * 用于监听日志消息队列，达到设定的数就开始执行刷入硬盘的操作
         *
         * @author majianguo
         * @since 2020/11/2 9:32
         */
        private void listener() {
            try {
                // 判断如果队列里面的数据大于等于设定的最大消息数，就调用refresh方法刷新一次数据
                if (count.get() >= maxCount) {
                    refresh();
                }
            } catch (Exception e) {
                if (log.isDebugEnabled()) {
                    e.printStackTrace();
                }
                log.error(e.getMessage());
            }
        }

        @Override
        @SuppressWarnings("InfiniteLoopStatement")
        public void run() {
            try {
                for (; ; ) {
                    // 消息监听器
                    listener();
                    // 定时任务监听器
                    timing();
                    TimeUnit.MILLISECONDS.sleep(10);
                }
            } catch (InterruptedException e) {
                if (log.isDebugEnabled()) {
                    e.printStackTrace();
                }
                log.error(e.getMessage());
            }
        }
    }

}
