/*
 * Copyright (C) 2014 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;

import com.android.ide.common.rendering.api.AssetRepository;
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.content.res.AssetManager;
import android.content.res.BridgeAssetManager;

import java.awt.Font;
import java.awt.FontFormatException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;

import static android.graphics.Typeface_Delegate.SYSTEM_FONTS;

/**
 * Delegate implementing the native methods of android.graphics.FontFamily
 *
 * Through the layoutlib_create tool, the original native methods of FontFamily have been replaced
 * by calls to methods of the same name in this delegate class.
 *
 * 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 class.
 *
 * @see DelegateManager
 */
public class FontFamily_Delegate {

    public static final int DEFAULT_FONT_WEIGHT = 400;
    public static final int BOLD_FONT_WEIGHT_DELTA = 300;
    public static final int BOLD_FONT_WEIGHT = 700;

    private static final String FONT_SUFFIX_ITALIC = "Italic.ttf";
    private static final String FN_ALL_FONTS_LIST = "fontsInSdk.txt";
    private static final String EXTENSION_OTF = ".otf";

    private static final int CACHE_SIZE = 10;
    // The cache has a drawback that if the font file changed after the font object was created,
    // we will not update it.
    private static final Map<String, FontInfo> sCache =
            new LinkedHashMap<String, FontInfo>(CACHE_SIZE) {
        @Override
        protected boolean removeEldestEntry(Map.Entry<String, FontInfo> eldest) {
            return size() > CACHE_SIZE;
        }

        @Override
        public FontInfo put(String key, FontInfo value) {
            // renew this entry.
            FontInfo removed = remove(key);
            super.put(key, value);
            return removed;
        }
    };

    /**
     * A class associating {@link Font} with its metadata.
     */
    private static final class FontInfo {
        @Nullable
        Font mFont;
        int mWeight;
        boolean mIsItalic;
    }

    // ---- delegate manager ----
    private static final DelegateManager<FontFamily_Delegate> sManager =
            new DelegateManager<FontFamily_Delegate>(FontFamily_Delegate.class);

    // ---- delegate helper data ----
    private static String sFontLocation;
    private static final List<FontFamily_Delegate> sPostInitDelegate = new
            ArrayList<FontFamily_Delegate>();
    private static Set<String> SDK_FONTS;


    // ---- delegate data ----
    private List<FontInfo> mFonts = new ArrayList<FontInfo>();

    /**
     * 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;
    // List of runnables to process fonts after sFontLoader is initialized.
    private List<Runnable> mPostInitRunnables = new ArrayList<Runnable>();
    /** @see #isValid() */
    private boolean mValid = false;


    // ---- Public helper class ----

    public enum FontVariant {
        // The order needs to be kept in sync with android.graphics.FontFamily.
        NONE, COMPACT, ELEGANT
    }

    // ---- Public Helper methods ----

    public static FontFamily_Delegate getDelegate(long nativeFontFamily) {
        return sManager.getDelegate(nativeFontFamily);
    }

    public static synchronized void setFontLocation(String fontLocation) {
        sFontLocation = fontLocation;
        // init list of bundled fonts.
        File allFonts = new File(fontLocation, FN_ALL_FONTS_LIST);
        // Current number of fonts is 103. Use the next round number to leave scope for more fonts
        // in the future.
        Set<String> allFontsList = new HashSet<String>(128);
        Scanner scanner = null;
        try {
            scanner = new Scanner(allFonts);
            while (scanner.hasNext()) {
                String name = scanner.next();
                // Skip font configuration files.
                if (!name.endsWith(".xml")) {
                    allFontsList.add(name);
                }
            }
        } catch (FileNotFoundException e) {
            Bridge.getLog().error(LayoutLog.TAG_BROKEN,
                    "Unable to load the list of fonts. Try re-installing the SDK Platform from the SDK Manager.",
                    e, null);
        } finally {
            if (scanner != null) {
                scanner.close();
            }
        }
        SDK_FONTS = Collections.unmodifiableSet(allFontsList);
        for (FontFamily_Delegate fontFamily : sPostInitDelegate) {
            fontFamily.init();
        }
        sPostInitDelegate.clear();
    }

    @Nullable
    public Font getFont(int desiredWeight, boolean isItalic) {
        FontInfo desiredStyle = new FontInfo();
        desiredStyle.mWeight = desiredWeight;
        desiredStyle.mIsItalic = isItalic;
        FontInfo bestFont = null;
        int bestMatch = Integer.MAX_VALUE;
        //noinspection ForLoopReplaceableByForEach (avoid iterator instantiation)
        for (int i = 0, n = mFonts.size(); i < n; i++) {
            FontInfo font = mFonts.get(i);
            int match = computeMatch(font, desiredStyle);
            if (match < bestMatch) {
                bestMatch = match;
                bestFont = font;
            }
        }
        if (bestFont == null) {
            return null;
        }
        if (bestMatch == 0) {
            return bestFont.mFont;
        }
        // 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;
    }

    /**
     * Returns if the FontFamily should contain any fonts. If this returns true and
     * {@link #getFont(int, boolean)} returns an empty list, it means that an error occurred while
     * loading the fonts. However, some fonts are deliberately skipped, for example they are not
     * bundled with the SDK. In such a case, this method returns false.
     */
    public boolean isValid() {
        return mValid;
    }

    private static Font loadFont(String path) {
        if (path.startsWith(SYSTEM_FONTS) ) {
            String relativePath = path.substring(SYSTEM_FONTS.length());
            File f = new File(sFontLocation, relativePath);

            try {
                return Font.createFont(Font.TRUETYPE_FONT, f);
            } catch (Exception e) {
                if (path.endsWith(EXTENSION_OTF) && e instanceof FontFormatException) {
                    // If we aren't able to load an Open Type font, don't log a warning just yet.
                    // We wait for a case where font is being used. Only then we try to log the
                    // warning.
                    return null;
                }
                Bridge.getLog().fidelityWarning(LayoutLog.TAG_BROKEN,
                        String.format("Unable to load font %1$s", relativePath),
                        e, null);
            }
        } else {
            Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
                    "Only platform fonts located in " + SYSTEM_FONTS + "can be loaded.",
                    null, null);
        }

        return null;
    }

    @Nullable
    /*package*/ static String getFontLocation() {
        return sFontLocation;
    }

    // ---- delegate methods ----
    @LayoutlibDelegate
    /*package*/ static boolean addFont(FontFamily thisFontFamily, String path, int ttcIndex) {
        final FontFamily_Delegate delegate = getDelegate(thisFontFamily.mNativePtr);
        return delegate != null && delegate.addFont(path, ttcIndex);
    }

    // ---- native methods ----

    @LayoutlibDelegate
    /*package*/ static long nCreateFamily(String lang, int variant) {
        // TODO: support lang. This is required for japanese locale.
        FontFamily_Delegate delegate = new FontFamily_Delegate();
        // variant can be 0, 1 or 2.
        assert variant < 3;
        delegate.mVariant = FontVariant.values()[variant];
        if (sFontLocation != null) {
            delegate.init();
        } else {
            sPostInitDelegate.add(delegate);
        }
        return sManager.addNewDelegate(delegate);
    }

    @LayoutlibDelegate
    /*package*/ static void nUnrefFamily(long nativePtr) {
        // Removing the java reference for the object doesn't mean that it's freed for garbage
        // collection. Typeface_Delegate may still hold a reference for it.
        sManager.removeJavaReferenceFor(nativePtr);
    }

    @LayoutlibDelegate
    /*package*/ static boolean nAddFont(long nativeFamily, ByteBuffer font, int ttcIndex) {
        assert false : "The only client of this method has been overriden.";
        return false;
    }

    @LayoutlibDelegate
    /*package*/ static boolean nAddFontWeightStyle(long nativeFamily, ByteBuffer font,
            int ttcIndex, List<FontListParser.Axis> listOfAxis,
            int weight, boolean isItalic) {
        assert false : "The only client of this method has been overriden.";
        return false;
    }

    static boolean addFont(long nativeFamily, final String path, final int weight,
            final boolean isItalic) {
        final FontFamily_Delegate delegate = getDelegate(nativeFamily);
        if (delegate != null) {
            if (sFontLocation == null) {
                delegate.mPostInitRunnables.add(() -> delegate.addFont(path, weight, isItalic));
                return true;
            }
            return delegate.addFont(path, weight, isItalic);
        }
        return false;
    }

    @LayoutlibDelegate
    /*package*/ static boolean nAddFontFromAsset(long nativeFamily, AssetManager mgr, String path) {
        FontFamily_Delegate ffd = sManager.getDelegate(nativeFamily);
        if (ffd == null) {
            return false;
        }
        ffd.mValid = true;
        if (mgr == null) {
            return false;
        }
        if (mgr instanceof BridgeAssetManager) {
            InputStream fontStream = null;
            try {
                AssetRepository assetRepository = ((BridgeAssetManager) mgr).getAssetRepository();
                if (assetRepository == null) {
                    Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Asset not found: " + path,
                            null);
                    return false;
                }
                if (!assetRepository.isSupported()) {
                    // Don't log any warnings on unsupported IDEs.
                    return false;
                }
                // Check cache
                FontInfo fontInfo = sCache.get(path);
                if (fontInfo != null) {
                    // renew the font's lease.
                    sCache.put(path, fontInfo);
                    ffd.addFont(fontInfo);
                    return true;
                }
                fontStream = assetRepository.openAsset(path, AssetManager.ACCESS_STREAMING);
                if (fontStream == null) {
                    Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Asset not found: " + path,
                            path);
                    return false;
                }
                Font font = Font.createFont(Font.TRUETYPE_FONT, fontStream);
                fontInfo = new FontInfo();
                fontInfo.mFont = font;
                fontInfo.mWeight = font.isBold() ? BOLD_FONT_WEIGHT : DEFAULT_FONT_WEIGHT;
                fontInfo.mIsItalic = font.isItalic();
                ffd.addFont(fontInfo);
                return true;
            } catch (IOException e) {
                Bridge.getLog().error(LayoutLog.TAG_MISSING_ASSET, "Unable to load font " + path, e,
                        path);
            } catch (FontFormatException e) {
                if (path.endsWith(EXTENSION_OTF)) {
                    // otf fonts are not supported on the user's config (JRE version + OS)
                    Bridge.getLog().fidelityWarning(LayoutLog.TAG_UNSUPPORTED,
                            "OpenType fonts are not supported yet: " + path, null, path);
                } else {
                    Bridge.getLog().error(LayoutLog.TAG_BROKEN,
                            "Unable to load font " + path, e, path);
                }
            } finally {
                if (fontStream != null) {
                    try {
                        fontStream.close();
                    } catch (IOException ignored) {
                    }
                }
            }
            return false;
        }
        // This should never happen. AssetManager is a final class (from user's perspective), and
        // we've replaced every creation of AssetManager with our implementation. We create an
        // exception and log it, but continue with rest of the rendering, without loading this font.
        Bridge.getLog().error(LayoutLog.TAG_BROKEN,
                "You have found a bug in the rendering library. Please file a bug at b.android.com.",
                new RuntimeException("Asset Manager is not an instance of BridgeAssetManager"),
                null);
        return false;
    }


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

    private void init() {
        for (Runnable postInitRunnable : mPostInitRunnables) {
            postInitRunnable.run();
        }
        mPostInitRunnables = null;
    }

    private boolean addFont(final String path, int ttcIndex) {
        // FIXME: support ttc fonts. Hack JRE??
        if (sFontLocation == null) {
            mPostInitRunnables.add(() -> addFont(path));
            return true;
        }
        return addFont(path);
    }

     private boolean addFont(@NonNull String path) {
         return addFont(path, DEFAULT_FONT_WEIGHT, path.endsWith(FONT_SUFFIX_ITALIC));
     }

    private boolean addFont(@NonNull String path, int weight, boolean isItalic) {
        if (path.startsWith(SYSTEM_FONTS) &&
                !SDK_FONTS.contains(path.substring(SYSTEM_FONTS.length()))) {
            return mValid = false;
        }
        // Set valid to true, even if the font fails to load.
        mValid = true;
        Font font = loadFont(path);
        if (font == null) {
            return false;
        }
        FontInfo fontInfo = new FontInfo();
        fontInfo.mFont = font;
        fontInfo.mWeight = weight;
        fontInfo.mIsItalic = isItalic;
        addFont(fontInfo);
        return true;
    }

    private boolean addFont(@NonNull FontInfo fontInfo) {
        int weight = fontInfo.mWeight;
        boolean isItalic = fontInfo.mIsItalic;
        // The list is usually just two fonts big. So iterating over all isn't as bad as it looks.
        // It's biggest for roboto where the size is 12.
        //noinspection ForLoopReplaceableByForEach (avoid iterator instantiation)
        for (int i = 0, n = mFonts.size(); i < n; i++) {
            FontInfo font = mFonts.get(i);
            if (font.mWeight == weight && font.mIsItalic == isItalic) {
                return false;
            }
        }
        mFonts.add(fontInfo);
        return true;
    }

    /**
     * Compute matching metric between two styles - 0 is an exact match.
     */
    private static int computeMatch(@NonNull FontInfo font1, @NonNull FontInfo font2) {
        int score = Math.abs(font1.mWeight - font2.mWeight);
        if (font1.mIsItalic != font2.mIsItalic) {
            score += 200;
        }
        return score;
    }

    /**
     * Try to derive a font from {@code srcFont} for the style in {@code outFont}.
     * <p/>
     * {@code outFont} is updated to reflect the style of the derived font.
     * @param srcFont the source font
     * @param outFont contains the desired font style. Updated to contain the derived font and
     *                its style
     * @return outFont
     */
    @NonNull
    private FontInfo deriveFont(@NonNull FontInfo srcFont, @NonNull FontInfo outFont) {
        int desiredWeight = outFont.mWeight;
        int srcWeight = srcFont.mWeight;
        assert srcFont.mFont != null;
        Font derivedFont = srcFont.mFont;
        // Embolden the font if required.
        if (desiredWeight >= BOLD_FONT_WEIGHT && desiredWeight - srcWeight > BOLD_FONT_WEIGHT_DELTA / 2) {
            derivedFont = derivedFont.deriveFont(Font.BOLD);
            srcWeight += BOLD_FONT_WEIGHT_DELTA;
        }
        // Italicize the font if required.
        if (outFont.mIsItalic && !srcFont.mIsItalic) {
            derivedFont = derivedFont.deriveFont(Font.ITALIC);
        } else if (outFont.mIsItalic != srcFont.mIsItalic) {
            // The desired font is plain, but the src font is italics. We can't convert it back. So
            // we update the value to reflect the true style of the font we're deriving.
            outFont.mIsItalic = srcFont.mIsItalic;
        }
        outFont.mFont = derivedFont;
        outFont.mWeight = srcWeight;
        // No need to update mIsItalics, as it's already been handled above.
        return outFont;
    }
}
