/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * 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 android.graphics.fonts;

import com.android.ide.common.rendering.api.LayoutLog;
import com.android.layoutlib.bridge.Bridge;
import com.android.layoutlib.bridge.impl.DelegateManager;
import com.android.tools.layoutlib.annotations.LayoutlibDelegate;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.FontFamily_Delegate.FontInfo;
import android.graphics.FontFamily_Delegate.FontVariant;
import android.graphics.Paint;

import java.awt.Font;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.nio.ByteBuffer;
import java.util.LinkedHashMap;
import java.util.Map;

import libcore.util.NativeAllocationRegistry_Delegate;

import static android.graphics.FontFamily_Delegate.computeMatch;
import static android.graphics.FontFamily_Delegate.deriveFont;

/**
 * Delegate implementing the native methods of android.graphics.fonts.FontFamily$Builder
 * <p>
 * Through the layoutlib_create tool, the original native methods of FontFamily$Builder have been
 * replaced by calls to methods of the same name in this delegate class.
 * <p>
 * This class behaves like the original native implementation, but in Java, keeping previously
 * native data into its own objects and mapping them to int that are sent back and forth between it
 * and the original FontFamily$Builder class.
 *
 * @see DelegateManager
 */
public class FontFamily_Builder_Delegate {
    private static final DelegateManager<FontFamily_Builder_Delegate> sBuilderManager =
            new DelegateManager<>(FontFamily_Builder_Delegate.class);

    private static long sFontFamilyFinalizer = -1;

    // Order does not really matter but we use a LinkedHashMap to get reproducible results across
    // render calls
    private Map<FontInfo, Font> mFonts = new LinkedHashMap<>();
    /**
     * The variant of the Font Family - compact or elegant.
     * <p/>
     * 0 is unspecified, 1 is compact and 2 is elegant. This needs to be kept in sync with values in
     * android.graphics.FontFamily
     *
     * @see Paint#setElegantTextHeight(boolean)
     */
    private FontVariant mVariant;
    private boolean mIsCustomFallback;

    @LayoutlibDelegate
    /*package*/ static long nInitBuilder() {
        return sBuilderManager.addNewDelegate(new FontFamily_Builder_Delegate());
    }

    @LayoutlibDelegate
    /*package*/ static void nAddFont(long builderPtr, long fontPtr) {
        FontFamily_Builder_Delegate familyBuilder = sBuilderManager.getDelegate(builderPtr);
        Font_Builder_Delegate fontBuilder = Font_Builder_Delegate.sBuilderManager.getDelegate(fontPtr);
        if (familyBuilder == null || fontBuilder == null) {
            return;
        }
        Font font;
        if (fontBuilder.filePath.equals("")) {
            font = loadFontBuffer(fontBuilder.mBuffer);

        } else {
            font = loadFontPath(fontBuilder.filePath);
        }
        if (font != null) {
            familyBuilder.addFont(font, fontBuilder.mWeight, fontBuilder.mItalic);
        }
    }

    @LayoutlibDelegate
    /*package*/ static long nBuild(long builderPtr, String langTags, int variant,
            boolean isCustomFallback) {
        FontFamily_Builder_Delegate builder = sBuilderManager.getDelegate(builderPtr);
        if (builder != null) {
            assert variant < 3;
            builder.mVariant = FontVariant.values()[variant];
            builder.mIsCustomFallback = isCustomFallback;
        }
        return builderPtr;
    }

    @LayoutlibDelegate
    /*package*/ static long nGetReleaseNativeFamily() {
        synchronized (Font_Builder_Delegate.class) {
            if (sFontFamilyFinalizer == -1) {
                sFontFamilyFinalizer = NativeAllocationRegistry_Delegate.createFinalizer(
                        sBuilderManager::removeJavaReferenceFor);
            }
        }
        return sFontFamilyFinalizer;
    }

    public static FontFamily_Builder_Delegate getDelegate(long nativeFontFamily) {
        return sBuilderManager.getDelegate(nativeFontFamily);
    }

    @Nullable
    public Font getFont(int desiredWeight, boolean isItalic) {
        FontInfo desiredStyle = new FontInfo();
        desiredStyle.mWeight = desiredWeight;
        desiredStyle.mIsItalic = isItalic;

        Font cachedFont = mFonts.get(desiredStyle);
        if (cachedFont != null) {
            return cachedFont;
        }

        FontInfo bestFont = null;

        if (mFonts.size() == 1) {
            // No need to compute the match since we only have one candidate
            bestFont = mFonts.keySet().iterator().next();
        } else {
            int bestMatch = Integer.MAX_VALUE;

            for (FontInfo font : mFonts.keySet()) {
                int match = computeMatch(font, desiredStyle);
                if (match < bestMatch) {
                    bestMatch = match;
                    bestFont = font;
                    if (bestMatch == 0) {
                        break;
                    }
                }
            }
        }

        if (bestFont == null) {
            return null;
        }


        // Derive the font as required and add it to the list of Fonts.
        deriveFont(bestFont, desiredStyle);
        addFont(desiredStyle);
        return desiredStyle.mFont;
    }

    public FontVariant getVariant() {
        return mVariant;
    }

    // ---- private helper methods ----

    private void addFont(@NonNull Font font, int weight, boolean italic) {
        FontInfo fontInfo = new FontInfo();
        fontInfo.mFont = font;
        fontInfo.mWeight = weight;
        fontInfo.mIsItalic = italic;
        addFont(fontInfo);
    }

    private void addFont(@NonNull FontInfo fontInfo) {
        mFonts.putIfAbsent(fontInfo, fontInfo.mFont);
    }

    private static Font loadFontBuffer(@NonNull ByteBuffer buffer) {
        try {
            byte[] byteArray = new byte[buffer.limit()];
            buffer.rewind();
            buffer.get(byteArray);
            return Font.createFont(Font.TRUETYPE_FONT, new ByteArrayInputStream(byteArray));
        } catch (Exception e) {
            Bridge.getLog().fidelityWarning(LayoutLog.TAG_BROKEN, "Unable to load font",
                    e, null, null);
        }

        return null;
    }

    private static Font loadFontPath(@NonNull String path) {
        try {
            File file = new File(path);
            return Font.createFont(Font.TRUETYPE_FONT, file);
        } catch (Exception e) {
            Bridge.getLog().fidelityWarning(LayoutLog.TAG_BROKEN, "Unable to load font",
                    e, null, null);
        }

        return null;
    }
}
