/*
 * Copyright (C) 2020 - Amir Hossein Aghajari
 *
 * 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.aghajari.emojiview.preset;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.text.Spannable;
import android.text.Spanned;
import android.text.style.DynamicDrawableSpan;
import android.text.style.ImageSpan;

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

import com.aghajari.emojiview.AXEmojiManager;
import com.aghajari.emojiview.emoji.EmojiData;
import com.aghajari.emojiview.utils.Utils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class AXPresetEmojiLoader {
    private static final HashMap<CharSequence, DrawableInfo> rects = new HashMap<>();
    private static int drawImgSize;
    private static int bigImgSize;
    private static final Paint placeholderPaint;
    private static Bitmap[][] emojiBmp;
    private static boolean[][] loadingEmoji;

    private static Context context;
    public static volatile DispatchQueue globalQueue = new DispatchQueue("emojiGlobalQueue");
    private static final Handler uiThread;

    static void init(Context context, EmojiData emojiData) {
        AXPresetEmojiLoader.context = context;
        drawImgSize = Utils.dp(context, 20);
        boolean isTablet = false;
        bigImgSize = Utils.dp(context, isTablet ? 40 : 34);

        rects.clear();
        int c = emojiData.getCategoriesLength();
        emojiBmp = new Bitmap[c][];
        loadingEmoji = new boolean[8][];

        for (int a = 0; a < emojiBmp.length; a++) {
            int emojiCount = emojiData.getEmojiCount(a);
            emojiBmp[a] = new Bitmap[emojiCount];
            loadingEmoji[a] = new boolean[emojiCount];
        }

        String[][] data = emojiData.getData();
        for (int j = 0; j < data.length; j++) {
            for (int i = 0; i < data[j].length; i++) {
                rects.put(data[j][i], new DrawableInfo((byte) j, (short) i, i));
            }
        }
    }

    static {
        uiThread = new Handler(Looper.getMainLooper());
        placeholderPaint = new Paint();
        placeholderPaint.setColor(0x00000000);
    }

    /**
     * try to load emoji before showing EmojiView
     */
    public static void preloadEmoji(String code) {
        preloadEmoji(code, null);
    }

    /**
     * try to load emoji before showing EmojiView
     */
    public static void preloadEmoji(String code, EmojiLoaderListener listener) {
        final DrawableInfo info = getDrawableInfo(code);
        if (info != null) {
            loadEmoji(code, info.page, info.page2, listener);
        }
    }

    private static void loadEmoji(final byte page, final short page2, final EmojiDrawable drawable) {
        if (emojiBmp[page][page2] == null) {
            if (loadingEmoji[page][page2]) {
                return;
            }
            loadingEmoji[page][page2] = true;
            globalQueue.postRunnable(() -> {
                loadEmojiInternal(page, page2);
                loadingEmoji[page][page2] = false;
                uiThread.post(() -> {
                    if (drawable != null) drawable.invalidateSelf();
                });
            });
        }
    }

    public interface EmojiLoaderListener {
        void onEmojiLoaded(AXPresetEmoji emoji);
    }

    private static class ListenerData {
        EmojiLoaderListener listener;
        String code;

        ListenerData(EmojiLoaderListener listener, String code) {
            this.listener = listener;
            this.code = code;
        }
    }

    private static List<ListenerData> loadingListeners = null;

    private static void loadEmoji(final String code, final byte page, final short page2, final EmojiLoaderListener listener) {
        if (emojiBmp[page][page2] == null) {
            if (loadingEmoji[page][page2]) {
                if (listener == null) return;
                if (loadingListeners == null) loadingListeners = new ArrayList<>();
                loadingListeners.add(new ListenerData(listener, code));
                return;
            }
            loadingEmoji[page][page2] = true;
            globalQueue.postRunnable(() -> {
                loadEmojiInternal(page, page2);
                loadingEmoji[page][page2] = false;
                uiThread.post(() -> {
                    callListeners(code, listener);
                    if (listener != null)
                        listener.onEmojiLoaded(new AXPresetEmoji(code, AXEmojiManager.getInstance().getEmojiData()));
                });
            });
        } else {
            if (listener != null)
                listener.onEmojiLoaded(new AXPresetEmoji(code, AXEmojiManager.getInstance().getEmojiData()));
        }
    }

    private static void callListeners(String code, EmojiLoaderListener listener) {
        if (loadingListeners != null && loadingListeners.size() > 0) {
            List<ListenerData> remove = new ArrayList<>();
            for (ListenerData data : loadingListeners) {
                if (data.code.equals(code) && data.listener != null && data.listener != listener) {
                    data.listener.onEmojiLoaded(new AXPresetEmoji(code, AXEmojiManager.getInstance().getEmojiData()));
                    remove.add(data);
                }
            }
            if (remove.size() > 0) {
                for (ListenerData removeData : remove) {
                    loadingListeners.remove(removeData);
                }
            }
        }
    }

    private static void loadEmojiInternal(final byte page, final short page2) {
        try {
            int imageResize = 1;
            if (context.getResources().getDisplayMetrics().density <= 1.0f) {
                imageResize = 2;
            }

            emojiBmp[page][page2] = AXEmojiManager.getInstance().getEmojiData()
                    .loadEmoji(context, page, page2, imageResize);
        } catch (Throwable x) {
            x.printStackTrace();
        }
    }

    public static String fixEmoji(String emoji) {
        char ch;
        int length = emoji.length();
        for (int a = 0; a < length; a++) {
            ch = emoji.charAt(a);
            if (ch >= 0xD83C && ch <= 0xD83E) {
                if (ch == 0xD83C && a < length - 1) {
                    ch = emoji.charAt(a + 1);
                    if (ch == 0xDE2F || ch == 0xDC04 || ch == 0xDE1A || ch == 0xDD7F) {
                        emoji = emoji.substring(0, a + 2) + "\uFE0F" + emoji.substring(a + 2);
                        length++;
                        a += 2;
                    } else {
                        a++;
                    }
                } else {
                    a++;
                }
            } else if (ch == 0x20E3) {
                return emoji;
            } else if (ch >= 0x203C && ch <= 0x3299) {
                if (AXEmojiManager.getInstance().getEmojiData().getEmojiToFE0FMap().containsKey(ch)) {
                    emoji = emoji.substring(0, a + 1) + "\uFE0F" + emoji.substring(a + 1);
                    length++;
                    a++;
                }
            }
        }
        return emoji;
    }

    /**
     * @return emoji drawable
     */
    public static @Nullable
    EmojiDrawable getEmojiDrawable(CharSequence code) {
        DrawableInfo info = getDrawableInfo(code);
        if (info == null) {
            //No drawable for emoji + code
            return null;
        }
        EmojiDrawable ed = new EmojiDrawable(info);
        ed.setBounds(0, 0, drawImgSize, drawImgSize);
        return ed;
    }

    /**
     * @return emoji bitmap or null if emoji hasn't loaded yet (or it's invalid).
     */
    public static @Nullable
    Bitmap getEmojiBitmap(CharSequence code) {
        DrawableInfo info = getDrawableInfo(code);
        if (info == null) return null;
        return emojiBmp[info.page][info.page2];
    }

    private static DrawableInfo getDrawableInfo(CharSequence code) {
        DrawableInfo info = rects.get(code);
        if (info == null) {
            CharSequence newCode = AXEmojiManager.getInstance().getEmojiData().getEmojiAliasMap().get(code);
            if (newCode != null) {
                info = AXPresetEmojiLoader.rects.get(newCode);
            }
        }
        return info;
    }

    /**
     * @return false if there is no emoji for this code
     */
    public static boolean isValidEmoji(CharSequence code) {
        DrawableInfo info = rects.get(code);
        if (info == null) {
            CharSequence newCode = AXEmojiManager.getInstance().getEmojiData().getEmojiAliasMap().get(code);
            if (newCode != null) {
                info = AXPresetEmojiLoader.rects.get(newCode);
            }
        }
        return info != null;
    }

    /**
     * @return fullSize emoji drawable
     */
    public static Drawable getEmojiBigDrawable(String code) {
        EmojiDrawable ed = getEmojiDrawable(code);
        if (ed == null) {
            CharSequence newCode = AXEmojiManager.getInstance().getEmojiData().getEmojiAliasMap().get(code);
            if (newCode != null) {
                ed = AXPresetEmojiLoader.getEmojiDrawable(newCode);
            }
        }
        if (ed == null) {
            return null;
        }
        ed.setBounds(0, 0, bigImgSize, bigImgSize);
        ed.fullSize = true;
        return ed;
    }

    /**
     * @return emoji drawable with custom bounds
     */
    public static Drawable getEmojiDrawable(String code, int size, boolean fullSize) {
        EmojiDrawable ed = getEmojiDrawable(code);
        if (ed == null) {
            CharSequence newCode = AXEmojiManager.getInstance().getEmojiData().getEmojiAliasMap().get(code);
            if (newCode != null) {
                ed = AXPresetEmojiLoader.getEmojiDrawable(newCode);
            }
        }
        if (ed == null) {
            return null;
        }
        ed.setBounds(0, 0, size, size);
        ed.fullSize = fullSize;
        return ed;
    }

    private static class EmojiDrawable extends Drawable {
        private final DrawableInfo info;
        private boolean fullSize = false;
        private static final Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
        private static final Rect rect = new Rect();

        public EmojiDrawable(DrawableInfo i) {
            info = i;
        }

        public DrawableInfo getDrawableInfo() {
            return info;
        }

        public int getSize() {
            return (fullSize ? bigImgSize : drawImgSize);
        }

        public Rect getDrawRect() {
            Rect original = getBounds();
            int cX = original.centerX(), cY = original.centerY();
            rect.left = cX - getSize() / 2;
            rect.right = cX + getSize() / 2;
            rect.top = cY - getSize() / 2;
            rect.bottom = cY + getSize() / 2;
            return rect;
        }

        @Override
        public void draw(@NonNull Canvas canvas) {
            /*if (useSystemEmoji) {
                //textPaint.setTextSize(getBounds().width());
                canvas.drawText(EmojiData.data[info.page][info.emojiIndex], getBounds().left, getBounds().bottom, textPaint);
                return;
            }*/
            if (emojiBmp[info.page][info.page2] == null) {
                loadEmoji(info.page, info.page2, this);
                canvas.drawRect(getBounds(), placeholderPaint);
                return;
            }

            Rect b;
            if (fullSize) {
                b = getDrawRect();
            } else {
                b = getBounds();
            }

            //if (!canvas.quickReject(b.left, b.top, b.right, b.bottom, Canvas.EdgeType.AA)) {
            canvas.drawBitmap(emojiBmp[info.page][info.page2], null, b, paint);
            //}
        }

        @Override
        public int getOpacity() {
            return PixelFormat.TRANSPARENT;
        }

        @Override
        public void setAlpha(int alpha) {

        }

        @Override
        public void setColorFilter(ColorFilter cf) {

        }
    }

    private static class DrawableInfo {
        public byte page;
        public short page2;
        public int emojiIndex;

        public DrawableInfo(byte p, short p2, int index) {
            page = p;
            page2 = p2;
            emojiIndex = index;
        }
    }

    public static CharSequence replaceEmoji(CharSequence cs, Paint.FontMetrics fontMetrics, int size, boolean createNew) {
        return replaceEmoji(cs, fontMetrics, size, createNew, null);
    }

    public static CharSequence replaceEmoji(CharSequence cs, Paint.FontMetrics fontMetrics, int size, boolean createNew, List<SpanLocation> spans) {
        if (cs == null || cs.length() == 0) {
            return cs;
        }
        Spannable s = null;
        if (spans == null) {
            if (!createNew && cs instanceof Spannable) {
                s = (Spannable) cs;
            } else {
                s = Spannable.Factory.getInstance().newSpannable(cs.toString());
            }
        }
        long buf = 0;
        int emojiCount = 0;
        char c;
        int startIndex = -1;
        int startLength = 0;
        int previousGoodIndex = 0;
        StringBuilder emojiCode = new StringBuilder(16);
        EmojiDrawable drawable;
        EmojiSpan span;
        int length = cs.length();
        boolean doneEmoji = false;

        try {
            for (int i = 0; i < length; i++) {
                c = cs.charAt(i);

                if (c >= 0xD83C && c <= 0xD83E || (buf != 0 && (buf & 0xFFFFFFFF00000000L) == 0 && (buf & 0xFFFF) == 0xD83C && (c >= 0xDDE6 && c <= 0xDDFF))) {
                    if (startIndex == -1) {
                        startIndex = i;
                    }
                    emojiCode.append(c);
                    startLength++;
                    buf <<= 16;
                    buf |= c;
                } else if (emojiCode.length() > 0 && (c == 0x2640 || c == 0x2642 || c == 0x2695)) {
                    emojiCode.append(c);
                    startLength++;
                    buf = 0;
                    doneEmoji = true;
                } else if (buf > 0 && (c & 0xF000) == 0xD000) {
                    emojiCode.append(c);
                    startLength++;
                    buf = 0;
                    doneEmoji = true;
                } else if (c == 0x20E3) {
                    if (i > 0) {
                        char c2 = cs.charAt(previousGoodIndex);
                        if ((c2 >= '0' && c2 <= '9') || c2 == '#' || c2 == '*') {
                            startIndex = previousGoodIndex;
                            startLength = i - previousGoodIndex + 1;
                            emojiCode.append(c2);
                            emojiCode.append(c);
                            doneEmoji = true;
                        }
                    }
                } else if ((c == 0x00A9 || c == 0x00AE || c >= 0x203C && c <= 0x3299)
                        && AXEmojiManager.getInstance().getEmojiData().getDataCharsMap().containsKey(c)) {
                    if (startIndex == -1) {
                        startIndex = i;
                    }
                    startLength++;
                    emojiCode.append(c);
                    doneEmoji = true;
                } else if (startIndex != -1) {
                    emojiCode.setLength(0);
                    startIndex = -1;
                    startLength = 0;
                    doneEmoji = false;
                }
                if (doneEmoji && i + 2 < length) {
                    char next = cs.charAt(i + 1);
                    if (next == 0xD83C) {
                        next = cs.charAt(i + 2);
                        if (next >= 0xDFFB && next <= 0xDFFF) {
                            emojiCode.append(cs.subSequence(i + 1, i + 3));
                            startLength += 2;
                            i += 2;
                        }
                    } else if (emojiCode.length() >= 2 && emojiCode.charAt(0) == 0xD83C && emojiCode.charAt(1) == 0xDFF4 && next == 0xDB40) {
                        i++;
                        while (true) {
                            emojiCode.append(cs.subSequence(i, i + 2));
                            startLength += 2;
                            i += 2;
                            if (i >= cs.length() || cs.charAt(i) != 0xDB40) {
                                i--;
                                break;
                            }
                        }

                    }
                }
                previousGoodIndex = i;
                char prevCh = c;
                for (int a = 0; a < 3; a++) {
                    if (i + 1 < length) {
                        c = cs.charAt(i + 1);
                        if (a == 1) {
                            if (c == 0x200D && emojiCode.length() > 0) {
                                emojiCode.append(c);
                                i++;
                                startLength++;
                                doneEmoji = false;
                            }
                        } else if (startIndex != -1 || prevCh == '*' || prevCh >= '1' && prevCh <= '9') {
                            if (c >= 0xFE00 && c <= 0xFE0F) {
                                i++;
                                startLength++;
                            }
                        }
                    }
                }
                if (doneEmoji && i + 2 < length && cs.charAt(i + 1) == 0xD83C) {
                    char next = cs.charAt(i + 2);
                    if (next >= 0xDFFB && next <= 0xDFFF) {
                        emojiCode.append(cs.subSequence(i + 1, i + 3));
                        startLength += 2;
                        i += 2;
                    }
                }
                if (doneEmoji) {
                    CharSequence code = emojiCode.subSequence(0, emojiCode.length());
                    drawable = AXPresetEmojiLoader.getEmojiDrawable(code);
                    if (drawable != null) {
                        span = new EmojiSpan(drawable, DynamicDrawableSpan.ALIGN_BOTTOM, size, fontMetrics);
                        if (s != null)
                            s.setSpan(span, startIndex, startIndex + startLength, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                        if (spans != null)
                            spans.add(new SpanLocation(span, startIndex, startIndex + startLength));
                        emojiCount++;
                    }
                    startLength = 0;
                    startIndex = -1;
                    emojiCode.setLength(0);
                    doneEmoji = false;
                }
                if ((Build.VERSION.SDK_INT < 23 || Build.VERSION.SDK_INT >= 29) && emojiCount >= 50) {
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            return cs;
        }
        return s == null ? cs : s;
    }

    static class SpanLocation {
        public EmojiSpan span;
        public int start;
        public int end;

        public SpanLocation(EmojiSpan s, int start, int end) {
            span = s;
            this.start = start;
            this.end = end;
        }
    }

    static class EmojiSpan extends ImageSpan {
        private Paint.FontMetrics fontMetrics;
        private int size = Utils.dp(context, 20);

        public EmojiSpan(EmojiDrawable d, int verticalAlignment, int s, Paint.FontMetrics original) {
            super(d, verticalAlignment);
            fontMetrics = original;
            if (original != null) {
                size = (int) (Math.abs(fontMetrics.descent) + Math.abs(fontMetrics.ascent));
                if (size == 0) {
                    size = Utils.dp(context, 20);
                }
            }
            if (s > 0) {
                size = s;
            }
        }

        public void replaceFontMetrics(Paint.FontMetrics newMetrics, int newSize) {
            fontMetrics = newMetrics;
            size = newSize;
        }

        @Override
        public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
            if (fm == null) {
                fm = new Paint.FontMetricsInt();
            }

            if (fontMetrics == null) {
                int sz = super.getSize(paint, text, start, end, fm);

                int offset = Utils.dp(context, 8);
                int w = Utils.dp(context, 10);
                fm.top = -w - offset;
                fm.bottom = w - offset;
                fm.ascent = -w - offset;
                fm.leading = 0;
                fm.descent = w - offset;

                return sz;
            } else {
                if (fm != null) {
                    fm.ascent = (int) fontMetrics.ascent;
                    fm.descent = (int) fontMetrics.descent;

                    fm.top = (int) fontMetrics.top;
                    fm.bottom = (int) fontMetrics.bottom;
                }
                if (getDrawable() != null) {
                    getDrawable().setBounds(0, 0, size, size);
                }
                return size;
            }
        }
    }
}