/*
 * Copyright (C) 2006 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 static android.content.res.FontResourcesParser.ProviderResourceEntry;
import static android.content.res.FontResourcesParser.FontFileResourceEntry;
import static android.content.res.FontResourcesParser.FontFamilyFilesResourceEntry;
import static android.content.res.FontResourcesParser.FamilyResourceEntry;

import static java.lang.annotation.RetentionPolicy.SOURCE;

import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.FontListParser;
import android.graphics.fonts.FontRequest;
import android.graphics.fonts.FontResult;
import android.os.Bundle;
import android.os.Handler;
import android.os.ParcelFileDescriptor;
import android.os.ResultReceiver;
import android.provider.FontsContract;
import android.text.FontConfig;
import android.util.Base64;
import android.util.Log;
import android.util.LongSparseArray;
import android.util.LruCache;
import android.util.SparseArray;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.Preconditions;

import libcore.io.IoUtils;

import org.xmlpull.v1.XmlPullParserException;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

/**
 * The Typeface class specifies the typeface and intrinsic style of a font.
 * This is used in the paint, along with optionally Paint settings like
 * textSize, textSkewX, textScaleX to specify
 * how text appears when drawn (and measured).
 */
public class Typeface {

    private static String TAG = "Typeface";

    /** The default NORMAL typeface object */
    public static final Typeface DEFAULT;
    /**
     * The default BOLD typeface object. Note: this may be not actually be
     * bold, depending on what fonts are installed. Call getStyle() to know
     * for sure.
     */
    public static final Typeface DEFAULT_BOLD;
    /** The NORMAL style of the default sans serif typeface. */
    public static final Typeface SANS_SERIF;
    /** The NORMAL style of the default serif typeface. */
    public static final Typeface SERIF;
    /** The NORMAL style of the default monospace typeface. */
    public static final Typeface MONOSPACE;

    static Typeface[] sDefaults;
    private static final LongSparseArray<SparseArray<Typeface>> sTypefaceCache =
            new LongSparseArray<>(3);
    @GuardedBy("sLock")
    private static FontsContract sFontsContract;
    @GuardedBy("sLock")
    private static Handler sHandler;

    /**
     * Cache for Typeface objects dynamically loaded from assets. Currently max size is 16.
     */
    private static final LruCache<String, Typeface> sDynamicTypefaceCache = new LruCache<>(16);

    static Typeface sDefaultTypeface;
    static Map<String, Typeface> sSystemFontMap;
    static FontFamily[] sFallbackFonts;
    private static final Object sLock = new Object();

    static final String FONTS_CONFIG = "fonts.xml";

    /**
     * @hide
     */
    public long native_instance;

    // Style
    public static final int NORMAL = 0;
    public static final int BOLD = 1;
    public static final int ITALIC = 2;
    public static final int BOLD_ITALIC = 3;

    private int mStyle = 0;

    private int[] mSupportedAxes;
    private static final int[] EMPTY_AXES = {};

    private static void setDefault(Typeface t) {
        sDefaultTypeface = t;
        nativeSetDefault(t.native_instance);
    }

    /** Returns the typeface's intrinsic style attributes */
    public int getStyle() {
        return mStyle;
    }

    /** Returns true if getStyle() has the BOLD bit set. */
    public final boolean isBold() {
        return (mStyle & BOLD) != 0;
    }

    /** Returns true if getStyle() has the ITALIC bit set. */
    public final boolean isItalic() {
        return (mStyle & ITALIC) != 0;
    }

    /**
     * @hide
     * Used by Resources to load a font resource of type font file.
     */
    @Nullable
    public static Typeface createFromResources(AssetManager mgr, String path, int cookie) {
        if (sFallbackFonts != null) {
            synchronized (sDynamicTypefaceCache) {
                final String key = Builder.createAssetUid(
                        mgr, path, 0 /* ttcIndex */, null /* axes */);
                Typeface typeface = sDynamicTypefaceCache.get(key);
                if (typeface != null) return typeface;

                FontFamily fontFamily = new FontFamily();
                // TODO: introduce ttc index and variation settings to resource type font.
                if (fontFamily.addFontFromAssetManager(mgr, path, cookie, false /* isAsset */,
                        0 /* ttcIndex */, Builder.RESOLVE_BY_FONT_TABLE /* weight */,
                        Builder.RESOLVE_BY_FONT_TABLE /* italic */, null /* axes */)) {
                    fontFamily.freeze();
                    FontFamily[] families = {fontFamily};
                    typeface = createFromFamiliesWithDefault(families);
                    sDynamicTypefaceCache.put(key, typeface);
                    return typeface;
                }
            }
        }
        return null;
    }

    /**
     * @hide
     * Used by Resources to load a font resource of type xml.
     */
    @Nullable
    public static Typeface createFromResources(
            FamilyResourceEntry entry, AssetManager mgr, String path) {
        if (sFallbackFonts != null) {
            Typeface typeface = findFromCache(mgr, path);
            if (typeface != null) return typeface;

            if (entry instanceof ProviderResourceEntry) {
                final ProviderResourceEntry providerEntry = (ProviderResourceEntry) entry;
                // Downloadable font
                typeface = findFromCache(providerEntry.getAuthority(), providerEntry.getQuery());
                if (typeface != null) {
                    return typeface;
                }
                List<List<String>> givenCerts = providerEntry.getCerts();
                List<List<byte[]>> certs = new ArrayList<>();
                if (givenCerts != null) {
                    for (int i = 0; i < givenCerts.size(); i++) {
                        List<String> certSet = givenCerts.get(i);
                        List<byte[]> byteArraySet = new ArrayList<>();
                        for (int j = 0; j < certSet.size(); j++) {
                            byteArraySet.add(Base64.decode(certSet.get(j), Base64.DEFAULT));
                        }
                        certs.add(byteArraySet);
                    }
                }
                // Downloaded font and it wasn't cached, request it again and return a
                // default font instead (nothing we can do now).
                create(new FontRequest(providerEntry.getAuthority(), providerEntry.getPackage(),
                        providerEntry.getQuery(), certs), NO_OP_REQUEST_CALLBACK);
                return DEFAULT;
            }

            // family is FontFamilyFilesResourceEntry
            final FontFamilyFilesResourceEntry filesEntry =
                    (FontFamilyFilesResourceEntry) entry;

            FontFamily fontFamily = new FontFamily();
            for (final FontFileResourceEntry fontFile : filesEntry.getEntries()) {
                if (!fontFamily.addFontFromAssetManager(mgr, fontFile.getFileName(),
                        0 /* resourceCookie */, false /* isAsset */, 0 /* ttcIndex */,
                        fontFile.getWeight(),
                        fontFile.isItalic() ? Builder.ITALIC : Builder.NORMAL,
                        null /* axes */)) {
                    return null;
                }
            }
            fontFamily.freeze();
            FontFamily[] familyChain = { fontFamily };
            typeface = createFromFamiliesWithDefault(familyChain);
            synchronized (sDynamicTypefaceCache) {
                final String key = Builder.createAssetUid(mgr, path, 0 /* ttcIndex */,
                        null /* axes */);
                sDynamicTypefaceCache.put(key, typeface);
            }
            return typeface;
        }
        return null;
    }

    /**
     * Used by resources for cached loading if the font is available.
     * @hide
     */
    public static Typeface findFromCache(AssetManager mgr, String path) {
        synchronized (sDynamicTypefaceCache) {
            final String key = Builder.createAssetUid(mgr, path, 0 /* ttcIndex */, null /* axes */);
            Typeface typeface = sDynamicTypefaceCache.get(key);
            if (typeface != null) {
                return typeface;
            }
        }
        return null;
    }

    /**
     * Set the application context so we can generate font requests from the provider. This should
     * be called from ActivityThread when the application binds, as we preload fonts.
     * @hide
     */
    public static void setApplicationContext(Context context) {
        synchronized (sLock) {
            if (sFontsContract == null) {
                sFontsContract = new FontsContract(context);
                sHandler = new Handler();
            }
        }
    }

    /**
     * Create a typeface object given a font request. The font will be asynchronously fetched,
     * therefore the result is delivered to the given callback. See {@link FontRequest}.
     * Only one of the methods in callback will be invoked, depending on whether the request
     * succeeds or fails. These calls will happen on the main thread.
     * @param request A {@link FontRequest} object that identifies the provider and query for the
     *                request. May not be null.
     * @param callback A callback that will be triggered when results are obtained. May not be null.
     */
    public static void create(@NonNull FontRequest request, @NonNull FontRequestCallback callback) {
        // Check the cache first
        // TODO: would the developer want to avoid a cache hit and always ask for the freshest
        // result?
        Typeface cachedTypeface = findFromCache(
                request.getProviderAuthority(), request.getQuery());
        if (cachedTypeface != null) {
            sHandler.post(() -> callback.onTypefaceRetrieved(cachedTypeface));
            return;
        }
        synchronized (sLock) {
            if (sFontsContract == null) {
                throw new RuntimeException("Context not initialized, can't query provider");
            }
            final ResultReceiver receiver = new ResultReceiver(null) {
                @Override
                public void onReceiveResult(int resultCode, Bundle resultData) {
                    sHandler.post(() -> receiveResult(request, callback, resultCode, resultData));
                }
            };
            sFontsContract.getFont(request, receiver);
        }
    }

    private static Typeface findFromCache(String providerAuthority, String query) {
        synchronized (sDynamicTypefaceCache) {
            final String key = createProviderUid(providerAuthority, query);
            Typeface typeface = sDynamicTypefaceCache.get(key);
            if (typeface != null) {
                return typeface;
            }
        }
        return null;
    }

    private static void receiveResult(FontRequest request, FontRequestCallback callback,
            int resultCode, Bundle resultData) {
        Typeface cachedTypeface = findFromCache(
                request.getProviderAuthority(), request.getQuery());
        if (cachedTypeface != null) {
            // We already know the result.
            // Probably the requester requests the same font again in a short interval.
            callback.onTypefaceRetrieved(cachedTypeface);
            return;
        }
        if (resultCode != FontsContract.Columns.RESULT_CODE_OK) {
            callback.onTypefaceRequestFailed(resultCode);
            return;
        }
        if (resultData == null) {
            callback.onTypefaceRequestFailed(
                    FontRequestCallback.FAIL_REASON_FONT_NOT_FOUND);
            return;
        }
        List<FontResult> resultList =
                resultData.getParcelableArrayList(FontsContract.PARCEL_FONT_RESULTS);
        if (resultList == null || resultList.isEmpty()) {
            callback.onTypefaceRequestFailed(
                    FontRequestCallback.FAIL_REASON_FONT_NOT_FOUND);
            return;
        }
        FontFamily fontFamily = new FontFamily();
        for (int i = 0; i < resultList.size(); ++i) {
            FontResult result = resultList.get(i);
            ParcelFileDescriptor fd = result.getFileDescriptor();
            if (fd == null) {
                callback.onTypefaceRequestFailed(
                        FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR);
                return;
            }
            try (FileInputStream is = new FileInputStream(fd.getFileDescriptor())) {
                FileChannel fileChannel = is.getChannel();
                long fontSize = fileChannel.size();
                ByteBuffer fontBuffer = fileChannel.map(
                        FileChannel.MapMode.READ_ONLY, 0, fontSize);
                int style = result.getStyle();
                int weight = (style & BOLD) != 0 ? 700 : 400;
                final ArrayList<FontConfig.Axis> axes = FontListParser.parseFontVariationSettings(
                        result.getFontVariationSettings());
                if (!fontFamily.addFontFromBuffer(fontBuffer, result.getTtcIndex(),
                                axes.toArray(new FontConfig.Axis[axes.size()]), weight,
                                (style & ITALIC) == 0 ? Builder.NORMAL : Builder.ITALIC)) {
                    Log.e(TAG, "Error creating font " + request.getQuery());
                    callback.onTypefaceRequestFailed(
                            FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR);
                    return;
                }
            } catch (IOException e) {
                Log.e(TAG, "Error reading font " + request.getQuery(), e);
                callback.onTypefaceRequestFailed(
                        FontRequestCallback.FAIL_REASON_FONT_LOAD_ERROR);
                return;
            } finally {
                IoUtils.closeQuietly(fd);
            }
        }
        fontFamily.freeze();
        Typeface typeface = Typeface.createFromFamiliesWithDefault(new FontFamily[] { fontFamily });
        synchronized (sDynamicTypefaceCache) {
            String key = createProviderUid(request.getProviderAuthority(), request.getQuery());
            sDynamicTypefaceCache.put(key, typeface);
        }
        callback.onTypefaceRetrieved(typeface);
    }

    /**
     * Interface used to receive asynchronously fetched typefaces.
     */
    public interface FontRequestCallback {
        /**
         * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the given
         * provider was not found on the device.
         */
        int FAIL_REASON_PROVIDER_NOT_FOUND = FontsContract.RESULT_CODE_PROVIDER_NOT_FOUND;
        /**
         * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the given
         * provider must be authenticated and the given certificates do not match its signature.
         */
        int FAIL_REASON_WRONG_CERTIFICATES = FontsContract.RESULT_CODE_WRONG_CERTIFICATES;
        /**
         * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the font
         * returned by the provider was not loaded properly.
         */
        int FAIL_REASON_FONT_LOAD_ERROR = -3;
        /**
         * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the font
         * provider did not return any results for the given query.
         */
        int FAIL_REASON_FONT_NOT_FOUND = FontsContract.Columns.RESULT_CODE_FONT_NOT_FOUND;
        /**
         * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the font
         * provider found the queried font, but it is currently unavailable.
         */
        int FAIL_REASON_FONT_UNAVAILABLE = FontsContract.Columns.RESULT_CODE_FONT_UNAVAILABLE;
        /**
         * Constant returned by {@link #onTypefaceRequestFailed(int)} signaling that the given
         * query was not supported by the provider.
         */
        int FAIL_REASON_MALFORMED_QUERY = FontsContract.Columns.RESULT_CODE_MALFORMED_QUERY;

        /** @hide */
        @IntDef({ FAIL_REASON_PROVIDER_NOT_FOUND, FAIL_REASON_FONT_LOAD_ERROR,
                FAIL_REASON_FONT_NOT_FOUND, FAIL_REASON_FONT_UNAVAILABLE,
                FAIL_REASON_MALFORMED_QUERY })
        @Retention(RetentionPolicy.SOURCE)
        @interface FontRequestFailReason {}

        /**
         * Called then a Typeface request done via {@link Typeface#create(FontRequest,
         * FontRequestCallback)} is complete. Note that this method will not be called if
         * {@link #onTypefaceRequestFailed(int)} is called instead.
         * @param typeface  The Typeface object retrieved.
         */
        void onTypefaceRetrieved(Typeface typeface);

        /**
         * Called when a Typeface request done via {@link Typeface#create(FontRequest,
         * FontRequestCallback)} fails.
         * @param reason One of {@link #FAIL_REASON_PROVIDER_NOT_FOUND},
         *               {@link #FAIL_REASON_FONT_NOT_FOUND},
         *               {@link #FAIL_REASON_FONT_LOAD_ERROR},
         *               {@link #FAIL_REASON_FONT_UNAVAILABLE} or
         *               {@link #FAIL_REASON_MALFORMED_QUERY}.
         */
        void onTypefaceRequestFailed(@FontRequestFailReason int reason);
    }

    private static final FontRequestCallback NO_OP_REQUEST_CALLBACK = new FontRequestCallback() {
        @Override
        public void onTypefaceRetrieved(Typeface typeface) {
            // Do nothing.
        }

        @Override
        public void onTypefaceRequestFailed(@FontRequestFailReason int reason) {
            // Do nothing.
        }
    };

    /**
     * A builder class for creating new Typeface instance.
     *
     * Examples,
     * 1) Create Typeface from ttf file.
     * <pre>
     * <code>
     * Typeface.Builder buidler = new Typeface.Builder.obtain();
     * builder.setSourceFromFilePath("your_font_file.ttf");
     * Typeface typeface = builder.build();
     * builder.recycle();
     * </code>
     * </pre>
     *
     * 2) Create Typeface from ttc file in assets directory.
     * <pre>
     * <code>
     * Typeface.Builder buidler = new Typeface.Builder.obtain();
     * builder.setSourceFromAsset(getAssets(), "your_font_file.ttc");
     * builder.setTtcIndex(2);  // set index of font collection.
     * Typeface typeface = builder.build();
     * builder.recycle();
     * </code>
     * </pre>
     *
     * 3) Create Typeface from existing Typeface with variation settings.
     * <pre>
     *
     * <p>Note that only one source can be specified for the single Typeface.</p>
     */
    public static final class Builder {
        /**
         * Value for weight and italic.
         *
         * Indicates the value is resolved by font metadata.
         */
        // Must be same with C++ constant in core/jni/android/graphics/FontFamily.cpp
        public static final int RESOLVE_BY_FONT_TABLE = -1;

        /**
         * Value for italic.
         *
         * Indicates the font style is not italic.
         */
        public static final int NORMAL = 0;

        /**
         * Value for italic.
         *
         * Indicates the font style is italic.
         */
        public static final int ITALIC = 1;

        private int mTtcIndex;
        private FontConfig.Axis[] mAxes;

        private AssetManager mAssetManager;
        private String mPath;
        private FileDescriptor mFd;
        private @IntRange(from = -1) int mWeight = RESOLVE_BY_FONT_TABLE;

        /** @hide */
        @Retention(SOURCE)
        @IntDef({RESOLVE_BY_FONT_TABLE, NORMAL, ITALIC})
        public @interface Italic {}
        private @Italic int mItalic = RESOLVE_BY_FONT_TABLE;

        private boolean mHasSourceSet = false;
        private boolean mRecycled = false;

        /** Use Builder.obtain() instead */
        private void Builder() {}

        private static AtomicReference<Builder> mCache = new AtomicReference<>();

        /**
         * Returns Typeface.Builder from pool.
         */
        public static Builder obtain() {
            final Builder builder = mCache.getAndSet(null);
            if (builder != null) {
                builder.mRecycled = false;
                return builder;
            }
            return new Builder();
        }

        /**
         * Resets the internal states.
         */
        public void reset() {
            checkNotRecycled();
            mTtcIndex = 0;
            mAxes = null;

            mAssetManager = null;
            mPath = null;
            mFd = null;

            mWeight = RESOLVE_BY_FONT_TABLE;
            mItalic = RESOLVE_BY_FONT_TABLE;

            mHasSourceSet = false;
        }

        /**
         * Returns the instance to the pool.
         */
        public void recycle() {
            reset();
            mRecycled = true;

            mCache.compareAndSet(null, this);
        }

        private void checkNotRecycled() {
            if (mRecycled) {
                throw new IllegalStateException("Don't use Builder after calling recycle()");
            }
        }

        private void checkSingleFontSource() {
            if (mHasSourceSet) {
                throw new IllegalStateException("Typeface can only built with single font source.");
            }
        }

        /**
         * Sets a font file as a source of Typeface.
         *
         * @param path The file object refers to the font file.
         */
        public Builder setSourceFromFile(@NonNull File path) {
            return setSourceFromFilePath(path.getAbsolutePath());
        }

        /**
         * Sets a font file as a source of Typeface.
         *
         * @param fd The file descriptor. The passed fd must be mmap-able.
         */
        public Builder setSourceFromFile(@NonNull FileDescriptor fd) {
            checkNotRecycled();
            checkSingleFontSource();
            mFd = fd;
            mHasSourceSet = true;
            return this;
        }

        /**
         * Sets a font file as a source of Typeface.
         *
         * @param path The full path to the font file.
         */
        public Builder setSourceFromFilePath(@NonNull String path) {
            checkNotRecycled();
            checkSingleFontSource();
            mPath = path;
            mHasSourceSet = true;
            return this;
        }

        /**
         * Sets an asset entry as a source of Typeface.
         *
         * @param assetManager The application's asset manager
         * @param path The file name of the font data in the asset directory
         */
        public Builder setSourceFromAsset(@NonNull AssetManager assetManager,
                @NonNull String path) {
            checkNotRecycled();
            checkSingleFontSource();
            mAssetManager = Preconditions.checkNotNull(assetManager);
            mPath = Preconditions.checkStringNotEmpty(path);
            mHasSourceSet = true;
            return this;
        }

        /**
         * Sets weight of the font.
         *
         * By passing {@link #RESOLVE_BY_FONT_TABLE}, weight value is resolved by OS/2 table in
         * font file if possible.
         * @param weight a weight value or {@link #RESOLVE_BY_FONT_TABLE}
         */
        public Builder setWeight(@IntRange(from = -1) int weight) {
            checkNotRecycled();
            mWeight = weight;
            return this;
        }

        /**
         * Sets italic information of the font.
         *
         * By passing {@link #RESOLVE_BY_FONT_TABLE}, italic or normal is determined by OS/2 table
         * in font file if possible.
         * @param italic One of {@link #NORMAL}, {@link #ITALIC}, {@link #RESOLVE_BY_FONT_TABLE}.
         *                 will be used.
         */
        public Builder setItalic(@Italic int italic) {
            checkNotRecycled();
            mItalic = italic;
            return this;
        }

        /**
         * Sets an idex of the font collection.
         *
         * Can not be used for Typeface source. build() method will return null for invalid index.
         * @param ttcIndex An index of the font collection. If the font source is not font
         *                 collection, do not call this method or specify 0.
         */
        public Builder setTtcIndex(@IntRange(from = 0) int ttcIndex) {
            checkNotRecycled();
            mTtcIndex = ttcIndex;
            return this;
        }

        /**
         * Sets a font variation settings.
         *
         * @param variationSettings See {@link android.widget.TextView#setFontVariationSettings}.
         */
        public Builder setFontVariationSettings(@Nullable String variationSettings) {
            checkNotRecycled();
            if (mAxes != null) {
                throw new IllegalStateException("Font variation settings are already set.");
            }
            final List<FontConfig.Axis> axesList = FontListParser.parseFontVariationSettings(
                    variationSettings);
            mAxes = axesList.toArray(new FontConfig.Axis[axesList.size()]);
            return this;
        }

        /**
         * Sets a font variation settings.
         *
         * @param axes An array of font variation axis tag-value pairs.
         */
        public Builder setFontVariationSettings(@Nullable FontConfig.Axis[] axes) {
            checkNotRecycled();
            if (mAxes != null) {
                throw new IllegalStateException("Font variation settings are already set.");
            }
            mAxes = axes;
            return this;
        }

        /**
         * Creates a unique id for a given AssetManager and asset path.
         *
         * @param mgr  AssetManager instance
         * @param path The path for the asset.
         * @param ttcIndex The TTC index for the font.
         * @param axes The font variation settings.
         * @return Unique id for a given AssetManager and asset path.
         */
        private static String createAssetUid(final AssetManager mgr, String path, int ttcIndex,
                @Nullable FontConfig.Axis[] axes) {
            final SparseArray<String> pkgs = mgr.getAssignedPackageIdentifiers();
            final StringBuilder builder = new StringBuilder();
            final int size = pkgs.size();
            for (int i = 0; i < size; i++) {
                builder.append(pkgs.valueAt(i));
                builder.append("-");
            }
            builder.append(path);
            builder.append("-");
            builder.append(Integer.toString(ttcIndex));
            builder.append("-");
            if (axes != null) {
                for (FontConfig.Axis axis : axes) {
                    builder.append(Integer.toHexString(axis.getTag()));
                    builder.append("-");
                    builder.append(Float.toString(axis.getStyleValue()));
                }
            }
            return builder.toString();
        }

        /**
         * Generates new Typeface from specified configuration.
         *
         * @return Newly created Typeface. May return null if some parameters are invalid.
         */
        public Typeface build() {
            checkNotRecycled();
            if (!mHasSourceSet) {
                return null;
            }

            if (mFd != null) {  // set source by setSourceFromFile(FileDescriptor)
                try (FileInputStream fis = new FileInputStream(mFd)) {
                    FileChannel channel = fis.getChannel();
                    long size = channel.size();
                    ByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0, size);

                    final FontFamily fontFamily = new FontFamily();
                    if (!fontFamily.addFontFromBuffer(buffer, mTtcIndex, mAxes, mWeight, mItalic)) {
                        fontFamily.abortCreation();
                        return null;
                    }
                    fontFamily.freeze();
                    FontFamily[] families = { fontFamily };
                    return createFromFamiliesWithDefault(families);
                } catch (IOException e) {
                    return null;
                }
            } else if (mAssetManager != null) {  // set source by setSourceFromAsset()
                final String key = createAssetUid(mAssetManager, mPath, mTtcIndex, mAxes);
                synchronized (sDynamicTypefaceCache) {
                    Typeface typeface = sDynamicTypefaceCache.get(key);
                    if (typeface != null) return typeface;
                    final FontFamily fontFamily = new FontFamily();
                    if (!fontFamily.addFontFromAssetManager(mAssetManager, mPath, mTtcIndex,
                            true /* isAsset */, mTtcIndex, mWeight, mItalic, mAxes)) {
                        fontFamily.abortCreation();
                        return null;
                    }
                    fontFamily.freeze();
                    FontFamily[] families = { fontFamily };
                    typeface = createFromFamiliesWithDefault(families);
                    sDynamicTypefaceCache.put(key, typeface);
                    return typeface;
                }
            } else if (mPath != null) {  // set source by setSourceFromFile(File)
                final FontFamily fontFamily = new FontFamily();
                if (!fontFamily.addFont(mPath, mTtcIndex, mAxes, mWeight, mItalic)) {
                    fontFamily.abortCreation();
                    return null;
                }
                fontFamily.freeze();
                FontFamily[] families = { fontFamily };
                return createFromFamiliesWithDefault(families);
            } else {
                throw new IllegalArgumentException("No source was set.");
            }
        }
    }

    /**
     * Create a typeface object given a family name, and option style information.
     * If null is passed for the name, then the "default" font will be chosen.
     * The resulting typeface object can be queried (getStyle()) to discover what
     * its "real" style characteristics are.
     *
     * @param familyName May be null. The name of the font family.
     * @param style  The style (normal, bold, italic) of the typeface.
     *               e.g. NORMAL, BOLD, ITALIC, BOLD_ITALIC
     * @return The best matching typeface.
     */
    public static Typeface create(String familyName, int style) {
        if (sSystemFontMap != null) {
            return create(sSystemFontMap.get(familyName), style);
        }
        return null;
    }

    /**
     * Create a typeface object that best matches the specified existing
     * typeface and the specified Style. Use this call if you want to pick a new
     * style from the same family of an existing typeface object. If family is
     * null, this selects from the default font's family.
     *
     * @param family May be null. The name of the existing type face.
     * @param style  The style (normal, bold, italic) of the typeface.
     *               e.g. NORMAL, BOLD, ITALIC, BOLD_ITALIC
     * @return The best matching typeface.
     */
    public static Typeface create(Typeface family, int style) {
        if (style < 0 || style > 3) {
            style = 0;
        }
        long ni = 0;
        if (family != null) {
            // Return early if we're asked for the same face/style
            if (family.mStyle == style) {
                return family;
            }

            ni = family.native_instance;
        }

        Typeface typeface;
        SparseArray<Typeface> styles = sTypefaceCache.get(ni);

        if (styles != null) {
            typeface = styles.get(style);
            if (typeface != null) {
                return typeface;
            }
        }

        typeface = new Typeface(nativeCreateFromTypeface(ni, style));
        if (styles == null) {
            styles = new SparseArray<Typeface>(4);
            sTypefaceCache.put(ni, styles);
        }
        styles.put(style, typeface);

        return typeface;
    }

    /** @hide */
    public static Typeface createFromTypefaceWithVariation(Typeface family,
            List<FontConfig.Axis> axes) {
        final long ni = family == null ? 0 : family.native_instance;
        return new Typeface(nativeCreateFromTypefaceWithVariation(ni, axes));
    }

    /**
     * Returns one of the default typeface objects, based on the specified style
     *
     * @return the default typeface that corresponds to the style
     */
    public static Typeface defaultFromStyle(int style) {
        return sDefaults[style];
    }

    /**
     * Create a new typeface from the specified font data.
     *
     * @param mgr  The application's asset manager
     * @param path The file name of the font data in the assets directory
     * @return The new typeface.
     */
    public static Typeface createFromAsset(AssetManager mgr, String path) {
        if (path == null) {
            throw new NullPointerException();  // for backward compatibility
        }
        if (sFallbackFonts != null) {
            final Builder builder = Builder.obtain();
            try {
                builder.setSourceFromAsset(mgr, path);
                Typeface typeface = builder.build();
                if (typeface != null) {
                    return typeface;
                }
            } finally {
                builder.recycle();
            }
        }
        // For the compatibility reasons, throw runtime exception if failed to create Typeface.
        throw new RuntimeException("Font asset not found " + path);
    }

    /**
     * Creates a unique id for a given font provider and query.
     */
    private static String createProviderUid(String authority, String query) {
        final StringBuilder builder = new StringBuilder();
        builder.append("provider:");
        builder.append(authority);
        builder.append("-");
        builder.append(query);
        return builder.toString();
    }

    /**
     * Create a new typeface from the specified font file.
     *
     * @param path The path to the font data.
     * @return The new typeface.
     */
    public static Typeface createFromFile(@Nullable File path) {
        // For the compatibility reasons, leaving possible NPE here.
        // See android.graphics.cts.TypefaceTest#testCreateFromFileByFileReferenceNull
        return createFromFile(path.getAbsolutePath());
    }

    /**
     * Create a new typeface from the specified font file.
     *
     * @param path The full path to the font data.
     * @return The new typeface.
     */
    public static Typeface createFromFile(@Nullable String path) {
        if (path == null) {
            // For the compatibility reasons, need to throw NPE if the argument is null.
            // See android.graphics.cts.TypefaceTest#testCreateFromFileByFileNameNull
            throw new NullPointerException();
        }
        if (sFallbackFonts != null) {
            final Builder builder = Builder.obtain();
            try {
                builder.setSourceFromFilePath(path);
                Typeface typeface = builder.build();
                if (typeface != null) {
                    // For the compatibility reasons, throw runtime exception if failed to create
                    // Typeface.
                    return typeface;
                }
            } finally {
                builder.recycle();
            }
        }
        throw new RuntimeException("Font not found " + path);
    }

    /**
     * Create a new typeface from an array of font families.
     *
     * @param families array of font families
     */
    private static Typeface createFromFamilies(FontFamily[] families) {
        long[] ptrArray = new long[families.length];
        for (int i = 0; i < families.length; i++) {
            ptrArray[i] = families[i].mNativePtr;
        }
        return new Typeface(nativeCreateFromArray(ptrArray));
    }

    /**
     * Create a new typeface from an array of font families, including
     * also the font families in the fallback list.
     *
     * @param families array of font families
     */
    private static Typeface createFromFamiliesWithDefault(FontFamily[] families) {
        long[] ptrArray = new long[families.length + sFallbackFonts.length];
        for (int i = 0; i < families.length; i++) {
            ptrArray[i] = families[i].mNativePtr;
        }
        for (int i = 0; i < sFallbackFonts.length; i++) {
            ptrArray[i + families.length] = sFallbackFonts[i].mNativePtr;
        }
        return new Typeface(nativeCreateFromArray(ptrArray));
    }

    // don't allow clients to call this directly
    private Typeface(long ni) {
        if (ni == 0) {
            throw new RuntimeException("native typeface cannot be made");
        }

        native_instance = ni;
        mStyle = nativeGetStyle(ni);
    }

    private static FontFamily makeFamilyFromParsed(FontConfig.Family family,
            Map<String, ByteBuffer> bufferForPath) {
        FontFamily fontFamily = new FontFamily(family.getLanguage(), family.getVariant());
        for (FontConfig.Font font : family.getFonts()) {
            String fullPathName = "/system/fonts/" + font.getFontName();
            ByteBuffer fontBuffer = bufferForPath.get(fullPathName);
            if (fontBuffer == null) {
                try (FileInputStream file = new FileInputStream(fullPathName)) {
                    FileChannel fileChannel = file.getChannel();
                    long fontSize = fileChannel.size();
                    fontBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fontSize);
                    bufferForPath.put(fullPathName, fontBuffer);
                } catch (IOException e) {
                    Log.e(TAG, "Error mapping font file " + fullPathName);
                    continue;
                }
            }
            if (!fontFamily.addFontFromBuffer(fontBuffer, font.getTtcIndex(), font.getAxes(),
                    font.getWeight(), font.isItalic() ? Builder.ITALIC : Builder.NORMAL)) {
                Log.e(TAG, "Error creating font " + fullPathName + "#" + font.getTtcIndex());
            }
        }
        fontFamily.freeze();
        return fontFamily;
    }

    /*
     * (non-Javadoc)
     *
     * This should only be called once, from the static class initializer block.
     */
    private static void init() {
        // Load font config and initialize Minikin state
        File systemFontConfigLocation = getSystemFontConfigLocation();
        File configFilename = new File(systemFontConfigLocation, FONTS_CONFIG);
        try {
            FileInputStream fontsIn = new FileInputStream(configFilename);
            FontConfig fontConfig = FontListParser.parse(fontsIn);

            Map<String, ByteBuffer> bufferForPath = new HashMap<String, ByteBuffer>();

            List<FontFamily> familyList = new ArrayList<FontFamily>();
            // Note that the default typeface is always present in the fallback list;
            // this is an enhancement from pre-Minikin behavior.
            for (int i = 0; i < fontConfig.getFamilies().length; i++) {
                FontConfig.Family f = fontConfig.getFamilies()[i];
                if (i == 0 || f.getName() == null) {
                    familyList.add(makeFamilyFromParsed(f, bufferForPath));
                }
            }
            sFallbackFonts = familyList.toArray(new FontFamily[familyList.size()]);
            setDefault(Typeface.createFromFamilies(sFallbackFonts));

            Map<String, Typeface> systemFonts = new HashMap<String, Typeface>();
            for (int i = 0; i < fontConfig.getFamilies().length; i++) {
                Typeface typeface;
                FontConfig.Family f = fontConfig.getFamilies()[i];
                if (f.getName() != null) {
                    if (i == 0) {
                        // The first entry is the default typeface; no sense in
                        // duplicating the corresponding FontFamily.
                        typeface = sDefaultTypeface;
                    } else {
                        FontFamily fontFamily = makeFamilyFromParsed(f, bufferForPath);
                        FontFamily[] families = { fontFamily };
                        typeface = Typeface.createFromFamiliesWithDefault(families);
                    }
                    systemFonts.put(f.getName(), typeface);
                }
            }
            for (FontConfig.Alias alias : fontConfig.getAliases()) {
                Typeface base = systemFonts.get(alias.getToName());
                Typeface newFace = base;
                int weight = alias.getWeight();
                if (weight != 400) {
                    newFace = new Typeface(nativeCreateWeightAlias(base.native_instance, weight));
                }
                systemFonts.put(alias.getName(), newFace);
            }
            sSystemFontMap = systemFonts;

        } catch (RuntimeException e) {
            Log.w(TAG, "Didn't create default family (most likely, non-Minikin build)", e);
            // TODO: normal in non-Minikin case, remove or make error when Minikin-only
        } catch (FileNotFoundException e) {
            Log.e(TAG, "Error opening " + configFilename, e);
        } catch (IOException e) {
            Log.e(TAG, "Error reading " + configFilename, e);
        } catch (XmlPullParserException e) {
            Log.e(TAG, "XML parse exception for " + configFilename, e);
        }
    }

    static {
        init();
        // Set up defaults and typefaces exposed in public API
        DEFAULT         = create((String) null, 0);
        DEFAULT_BOLD    = create((String) null, Typeface.BOLD);
        SANS_SERIF      = create("sans-serif", 0);
        SERIF           = create("serif", 0);
        MONOSPACE       = create("monospace", 0);

        sDefaults = new Typeface[] {
            DEFAULT,
            DEFAULT_BOLD,
            create((String) null, Typeface.ITALIC),
            create((String) null, Typeface.BOLD_ITALIC),
        };

    }

    private static File getSystemFontConfigLocation() {
        return new File("/system/etc/");
    }

    @Override
    protected void finalize() throws Throwable {
        try {
            nativeUnref(native_instance);
            native_instance = 0;  // Other finalizers can still call us.
        } finally {
            super.finalize();
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Typeface typeface = (Typeface) o;

        return mStyle == typeface.mStyle && native_instance == typeface.native_instance;
    }

    @Override
    public int hashCode() {
        /*
         * Modified method for hashCode with long native_instance derived from
         * http://developer.android.com/reference/java/lang/Object.html
         */
        int result = 17;
        result = 31 * result + (int) (native_instance ^ (native_instance >>> 32));
        result = 31 * result + mStyle;
        return result;
    }

    /** @hide */
    public boolean isSupportedAxes(int axis) {
        if (mSupportedAxes == null) {
            synchronized (this) {
                if (mSupportedAxes == null) {
                    mSupportedAxes = nativeGetSupportedAxes(native_instance);
                    if (mSupportedAxes == null) {
                        mSupportedAxes = EMPTY_AXES;
                    }
                }
            }
        }
        return Arrays.binarySearch(mSupportedAxes, axis) > 0;
    }

    private static native long nativeCreateFromTypeface(long native_instance, int style);
    // TODO: clean up: change List<FontConfig.Axis> to FontConfig.Axis[]
    private static native long nativeCreateFromTypefaceWithVariation(
            long native_instance, List<FontConfig.Axis> axes);
    private static native long nativeCreateWeightAlias(long native_instance, int weight);
    private static native void nativeUnref(long native_instance);
    private static native int  nativeGetStyle(long native_instance);
    private static native long nativeCreateFromArray(long[] familyArray);
    private static native void nativeSetDefault(long native_instance);
    private static native int[] nativeGetSupportedAxes(long native_instance);
}
