package cgmblelib.utils;

import static com.orhanobut.logger.Logger.ASSERT;
import static com.orhanobut.logger.Logger.DEBUG;
import static com.orhanobut.logger.Logger.ERROR;
import static com.orhanobut.logger.Logger.INFO;
import static com.orhanobut.logger.Logger.VERBOSE;
import static com.orhanobut.logger.Logger.WARN;

import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.orhanobut.logger.AndroidLogAdapter;
import com.orhanobut.logger.DiskLogAdapter;
import com.orhanobut.logger.FormatStrategy;
import com.orhanobut.logger.LogStrategy;
import com.orhanobut.logger.Logger;
import com.orhanobut.logger.PrettyFormatStrategy;

import org.json.JSONObject;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Locale;

import cgmblelib.base.BApplication;

/**
 * Created by minyuchun on 2017/6/21.
 * 日志生成
 *
 * @author minyuchun
 */
public class MyLog {
    /**
     * 日志工厂
     */
    private static FormatStrategy formatStrategy;
    /**
     * 存储的日志工厂
     */
    private static FormatStrategy localFormatStrategy;
    /**
     * APP是否处于debug模式
     */
    private static boolean isDebug = true;

    /**
     * 设置是否APP模式是否为debug模式
     *
     * @param isDebug 处于debug模式
     */
    public static void setIsDebug(boolean isDebug) {
        MyLog.isDebug = isDebug;
    }

    /**
     * 初始化普通的日志
     * 1.默认的日志输出
     */
    public static void initDefault() {
        if (formatStrategy == null) {
            formatStrategy = PrettyFormatStrategy.newBuilder()
                    .tag(isDebug ? "debug" : "release")
                    .build();
            Logger.addLogAdapter(new AndroidLogAdapter(formatStrategy));
        }
    }

    /**
     * 初始化特殊的日志
     * 1.自定义的日志输出格式
     */
    public static void initSpecial() {
        Logger.clearLogAdapters();
        if (formatStrategy == null) {
            formatStrategy = PrettyFormatStrategy.newBuilder()
                    //（可选）是否显示线程信息。 默认值为true
                    .showThreadInfo(true)
                    // （可选）要显示的方法行数。 默认2
                    .methodCount(4)
                    // （可选）隐藏内部方法调用到偏移量。 默认5
                    .methodOffset(1)
                    //（可选）每个日志的全局标记。 默认PRETTY_LOGGER
                    .tag(isDebug ? "debug" : "release")
                    .build();
        }
        Logger.addLogAdapter(new AndroidLogAdapter(formatStrategy));
    }

    /**
     * 设置本地日志记录
     *
     * @param deviceId   发射器的id
     * @param deviceName 发射器的名字
     */
    public static void setLocalLog(long deviceId, String deviceName) {
        String folder = null;
        if (BApplication.getContext() != null) {
            File externalFilesDir = BApplication.getContext().getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS);
            if (externalFilesDir != null && externalFilesDir.exists()) {
                folder = externalFilesDir.getAbsolutePath();
            }
        }
        initSpecial();
        if (localFormatStrategy == null) {
            localFormatStrategy = MyFormatStrategy.newBuilder()
                    .tag(isDebug ? "debug" : "release")
                    .deviceId(deviceId)
                    .deviceName(deviceName)
                    .folder(folder)
                    .build();
        }
        Logger.addLogAdapter(new DiskLogAdapter(localFormatStrategy));
    }

    /**
     * 清楚输出的日志格式设置
     * 如果普通日志格式存在，再次赋予
     */
    public static void clearLogAdapter() {
        initSpecial();
        if (!ConstantsLibrary.IS_ALLINCSV) {
            localFormatStrategy = null;
        }
    }

    public static void t(String tag) {
        if (isDebug) {
            tLog(tag);
        }
    }

    public synchronized static void tLog(String tag) {
        try {
            Logger.t(tag);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void v(String s, Object... os) {
        if (isDebug) {
            vLog(s, os);
        }
    }

    public synchronized static void vLog(String s, Object... os) {
        try {
            Logger.v(s, os);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    public static void d(String s, Object... os) {
        if (isDebug) {
            dLog(s, os);
        }
    }

    public synchronized static void dLog(String s, Object... os) {
        try {
            Logger.d(s, os);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void i(String s, Object... os) {
        if (isDebug) {
            iLog(s, os);
        }
    }

    public synchronized static void iLog(String s, Object... os) {
        try {
            Logger.i(s, os);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void w(String s, Object... os) {
        if (isDebug) {
            wLog(s, os);
        }
    }

    public synchronized static void wLog(String s, Object... os) {
        try {
            Logger.w(s, os);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void e(String s, Object... os) {
        if (isDebug) {
            Logger.e(s, os);
        }
    }

    public synchronized static void eLog(String s, Object... os) {
        try {
            Logger.e(s, os);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void e(Throwable t, String s, Object... os) {
        if (isDebug) {
            Logger.e(t, s, os);
        }
    }

    public synchronized static void eLog(Throwable t, String s, Object... os) {
        try {
            Logger.e(t, s, os);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void json(JSONObject json) {
        if (isDebug) {
            jsonLog(json);
        }
    }

    public synchronized static void jsonLog(JSONObject json) {
        try {
            Logger.json(json.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void xml(String xml) {
        if (isDebug) {
            xmlLog(xml);
        }
    }

    public synchronized static void xmlLog(String xml) {
        try {
            Logger.xml(xml);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

/**
 * 自定义日志工厂
 */
class MyFormatStrategy implements FormatStrategy {
    /**
     * 换行符
     */
    private static final String NEW_LINE = System.getProperty("line.separator");
    private static final String NEW_LINE_REPLACEMENT = " <br> ";
    /**
     * csv文件分隔符
     */
    private static final String SEPARATOR = ",";

    @NonNull
    private final Date date;
    @NonNull
    private final SimpleDateFormat dateFormat;
    @NonNull
    private final LogStrategy logStrategy;
    @Nullable
    private final String tag;

    private MyFormatStrategy(@NonNull Builder builder) {
        checkNotNull(builder);
        date = builder.date;
        dateFormat = builder.dateFormat;
        logStrategy = builder.logStrategy;
        tag = builder.tag;
    }

    @NonNull
    public static Builder newBuilder() {
        return new Builder();
    }

    @Override
    public void log(int priority, @Nullable String onceOnlyTag, @NonNull String message) {
        checkNotNull(message);
        String tag = formatTag(onceOnlyTag);
        date.setTime(System.currentTimeMillis());
        StringBuilder builder = new StringBuilder();
        //date
        builder.append(dateFormat.format(date));
        // level
        builder.append(SEPARATOR);
        builder.append(logLevel(priority));
        // tag
        builder.append(SEPARATOR);
        builder.append(tag);
        // message
        if (message.contains(NEW_LINE)) {
            // a new line would break the CSV format, so we replace it here
            message = message.replaceAll(NEW_LINE, NEW_LINE_REPLACEMENT);
        }
        builder.append(SEPARATOR);
        builder.append(message);
        // new line
        builder.append(NEW_LINE);
        logStrategy.log(priority, tag, builder.toString());
    }

    @Nullable
    private String formatTag(@Nullable String tag) {
        if (!isEmpty(tag) && !equals(this.tag, tag)) {
            return this.tag + "-" + tag;
        }
        return this.tag;
    }

    public static final class Builder {
        /**
         * 文件最大的大小
         */
        private static final int MAX_BYTES = ConstantsLibrary.IS_ALLINCSV ? 1024 * 1024 : 500 * 1024;
        /**
         * 最大可以存储的文件数目
         */
        private static final int MAX_FILE_COUNT = 10;
        Date date;
        SimpleDateFormat dateFormat;
        LogStrategy logStrategy;
        String tag = "PocTech";
        long deviceId = 0;
        String deviceName = "";

        String folder;

        private Builder() {
        }

        @NonNull
        public Builder date(@Nullable Date val) {
            date = val;
            return this;
        }

        @NonNull
        public Builder dateFormat(@Nullable SimpleDateFormat val) {
            dateFormat = val;
            return this;
        }

        @NonNull
        public Builder logStrategy(@Nullable LogStrategy val) {
            logStrategy = val;
            return this;
        }

        @NonNull
        public Builder tag(@Nullable String tag) {
            this.tag = tag;
            return this;
        }

        @NonNull
        public Builder deviceId(@Nullable long deviceId) {
            this.deviceId = deviceId;
            return this;
        }

        @NonNull
        public Builder deviceName(@Nullable String deviceName) {
            this.deviceName = deviceName;
            return this;
        }

        @NonNull
        public Builder folder(@Nullable String folder) {
            this.folder = folder;
            return this;
        }

        @NonNull
        public MyFormatStrategy build() {
            if (date == null) {
                date = new Date();
            }
            if (dateFormat == null) {
                dateFormat = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss.SSS", Locale.UK);
            }
            if (logStrategy == null) {
                if (folder == null) {
                    String diskPath = Environment.getExternalStorageDirectory().getAbsolutePath();
                    folder = diskPath + File.separatorChar + "PocData" + File.separatorChar + "logger";
                }
                HandlerThread ht = new HandlerThread("AndroidFileLogger." + folder);
                ht.start();
                Handler handler = new MyLogStrategy.WriteHandler(ht.getLooper(), folder, MAX_BYTES, MAX_FILE_COUNT, deviceId, deviceName);
                logStrategy = new MyLogStrategy(handler);
            }
            return new MyFormatStrategy(this);
        }
    }

    static String logLevel(int value) {
        switch (value) {
            case VERBOSE:
                return "VERBOSE";
            case DEBUG:
                return "DEBUG";
            case INFO:
                return "INFO";
            case WARN:
                return "WARN";
            case ERROR:
                return "ERROR";
            case ASSERT:
                return "ASSERT";
            default:
                return "UNKNOWN";
        }
    }

    static boolean equals(CharSequence a, CharSequence b) {
        if (a == b) return true;
        if (a != null && b != null) {
            int length = a.length();
            if (length == b.length()) {
                if (a instanceof String && b instanceof String) {
                    return a.equals(b);
                } else {
                    for (int i = 0; i < length; i++) {
                        if (a.charAt(i) != b.charAt(i)) {
                            return false;
                        }
                    }
                    return true;
                }
            }
        }
        return false;
    }

    static boolean isEmpty(CharSequence str) {
        return str == null || str.length() == 0;
    }

    @NonNull
    static <T> T checkNotNull(@Nullable final T obj) {
        if (obj == null) {
            throw new NullPointerException();
        }
        return obj;
    }
}

class MyLogStrategy implements LogStrategy {

    @NonNull
    private final Handler handler;

    public MyLogStrategy(@NonNull Handler handler) {
        this.handler = checkNotNull(handler);
    }

    @Override
    public void log(int level, @Nullable String tag, @NonNull String message) {
        checkNotNull(message);
        // do nothing on the calling thread, simply pass the tag/msg to the background thread
        handler.sendMessage(handler.obtainMessage(level, message));
    }

    static class WriteHandler extends Handler {
        @NonNull
        private final String folder;
        private final int maxFileSize;
        private final int maxFileCount;
        private final long deviceId;
        private final String deviceName;

        WriteHandler(@NonNull Looper looper, @NonNull String folder, int maxFileSize, int maxFileCount, long deviceId, String deviceName) {
            super(checkNotNull(looper));
            this.folder = checkNotNull(folder);
            this.maxFileSize = maxFileSize;
            this.maxFileCount = maxFileCount;
            this.deviceId = deviceId;
            this.deviceName = deviceName;
        }

        @SuppressWarnings("checkstyle:emptyblock")
        @Override
        public void handleMessage(@NonNull Message msg) {
            String content = (String) msg.obj;
            FileWriter fileWriter = null;
            File logFile = ConstantsLibrary.IS_ALLINCSV ? getLogFile(folder, deviceName) : getLogFile(folder, deviceId, deviceName);
            try {
                fileWriter = new FileWriter(logFile, true);
                writeLog(fileWriter, content);
                fileWriter.flush();
                fileWriter.close();
            } catch (IOException e) {
                if (fileWriter != null) {
                    try {
                        fileWriter.flush();
                        fileWriter.close();
                    } catch (IOException e1) { /* fail silently */ }
                }
            }
        }

        /**
         * This is always called on a single background thread.
         * Implementing classes must ONLY write to the fileWriter and nothing more.
         * The abstract class takes care of everything else including close the stream and catching IOException
         *
         * @param fileWriter an instance of FileWriter already initialised to the correct file
         */
        private void writeLog(@NonNull FileWriter fileWriter, @NonNull String content) throws IOException {
            checkNotNull(fileWriter);
            checkNotNull(content);
            fileWriter.append(content);
        }

        private File getLogFile(@NonNull String folderName, @NonNull String fileName) {
            checkNotNull(folderName);
            checkNotNull(fileName);

            File folder = new File(folderName);
            if (!folder.exists()) {
                //TODO: What if folder is not created, what happens then?
                folder.mkdirs();
            }

            int newFileCount = 0;
            File newFile;
            File existingFile = null;

            newFile = new File(folder, String.format("%s_%s.csv", fileName, newFileCount));
            while (newFile.exists()) {
                existingFile = newFile;
                newFileCount++;
                newFile = new File(folder, String.format("%s_%s.csv", fileName, newFileCount));
            }

            if (existingFile != null) {
                if (existingFile.length() >= maxFileSize) {
                    return newFile;
                }
                return existingFile;
            }

            return newFile;
        }

        private File getLogFile(@NonNull String folderName, long deviceId, @NonNull String deviceName) {
            checkNotNull(folderName);
            checkNotNull(deviceName);
            File folder = new File(folderName);
            File[] files = folder.listFiles(pathname -> pathname.getName().contains(".csv"));
            if (files.length > maxFileCount) {
                Arrays.sort(files, (o1, o2) -> {
                    long diff = o1.lastModified() - o2.lastModified();
                    if (diff < 0) {
                        return 1;
                    } else if (diff > 0) {
                        return -1;
                    }
                    return 0;
                });
                File deleteFile = files[files.length - 1];
                deleteFile.delete();
            }
            if (!folder.exists()) {
                //TODO: What if folder is not created, what happens then?
                folder.mkdirs();
            }
            File newFile;
            newFile = new File(folder, String.format("%s_%s.csv", Long.toString(deviceId), deviceName));
            return newFile;
        }
    }

    @NonNull
    static <T> T checkNotNull(@Nullable final T obj) {
        if (obj == null) {
            throw new NullPointerException();
        }
        return obj;
    }
}
