/*
 * Copyright (C) 2008 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 com.android.layoutlib.bridge.impl;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import android.graphics.Typeface;

import java.awt.Font;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

/**
 * Provides {@link Font} object to the layout lib.
 * <p/>
 * The fonts are loaded from the SDK directory. Family/style mapping is done by parsing the
 * fonts.xml file located alongside the ttf files.
 */
public final class FontLoader {
    private static final String FONTS_SYSTEM = "system_fonts.xml";
    private static final String FONTS_VENDOR = "vendor_fonts.xml";
    private static final String FONTS_FALLBACK = "fallback_fonts.xml";

    private static final String NODE_FAMILYSET = "familyset";
    private static final String NODE_FAMILY = "family";
    private static final String NODE_NAME = "name";
    private static final String NODE_FILE = "file";

    private static final String ATTRIBUTE_VARIANT = "variant";
    private static final String ATTRIBUTE_VALUE_ELEGANT = "elegant";
    private static final String FONT_SUFFIX_NONE = ".ttf";
    private static final String FONT_SUFFIX_REGULAR = "-Regular.ttf";
    private static final String FONT_SUFFIX_BOLD = "-Bold.ttf";
    // FONT_SUFFIX_ITALIC will always match FONT_SUFFIX_BOLDITALIC and hence it must be checked
    // separately.
    private static final String FONT_SUFFIX_ITALIC = "Italic.ttf";
    private static final String FONT_SUFFIX_BOLDITALIC = "-BoldItalic.ttf";

    // This must match the values of Typeface styles so that we can use them for indices in this
    // array.
    private static final int[] AWT_STYLES = new int[] {
        Font.PLAIN,
        Font.BOLD,
        Font.ITALIC,
        Font.BOLD | Font.ITALIC
    };
    private static int[] DERIVE_BOLD_ITALIC = new int[] {
        Typeface.ITALIC, Typeface.BOLD, Typeface.NORMAL
    };
    private static int[] DERIVE_ITALIC = new int[] { Typeface.NORMAL };
    private static int[] DERIVE_BOLD = new int[] { Typeface.NORMAL };

    private static final List<FontInfo> mMainFonts = new ArrayList<FontInfo>();
    private static final List<FontInfo> mFallbackFonts = new ArrayList<FontInfo>();

    private final String mOsFontsLocation;

    public static FontLoader create(String fontOsLocation) {
        try {
            SAXParserFactory parserFactory = SAXParserFactory.newInstance();
                parserFactory.setNamespaceAware(true);

            // parse the system fonts
            FontHandler handler = parseFontFile(parserFactory, fontOsLocation, FONTS_SYSTEM);
            List<FontInfo> systemFonts = handler.getFontList();


            // parse the fallback fonts
            handler = parseFontFile(parserFactory, fontOsLocation, FONTS_FALLBACK);
            List<FontInfo> fallbackFonts = handler.getFontList();

            return new FontLoader(fontOsLocation, systemFonts, fallbackFonts);
        } catch (ParserConfigurationException e) {
            // return null below
        } catch (SAXException e) {
            // return null below
        } catch (FileNotFoundException e) {
            // return null below
        } catch (IOException e) {
            // return null below
        }

        return null;
    }

    private static FontHandler parseFontFile(SAXParserFactory parserFactory,
            String fontOsLocation, String fontFileName)
            throws ParserConfigurationException, SAXException, IOException, FileNotFoundException {

        SAXParser parser = parserFactory.newSAXParser();
        File f = new File(fontOsLocation, fontFileName);

        FontHandler definitionParser = new FontHandler(
                fontOsLocation + File.separator);
        parser.parse(new FileInputStream(f), definitionParser);
        return definitionParser;
    }

    private FontLoader(String fontOsLocation,
            List<FontInfo> fontList, List<FontInfo> fallBackList) {
        mOsFontsLocation = fontOsLocation;
        mMainFonts.addAll(fontList);
        mFallbackFonts.addAll(fallBackList);
    }


    public String getOsFontsLocation() {
        return mOsFontsLocation;
    }

    /**
     * Returns a {@link Font} object given a family name and a style value (constant in
     * {@link Typeface}).
     * @param family the family name
     * @param style a 1-item array containing the requested style. Based on the font being read
     *              the actual style may be different. The array contains the actual style after
     *              the method returns.
     * @return the font object or null if no match could be found.
     */
    public synchronized List<Font> getFont(String family, int style) {
        List<Font> result = new ArrayList<Font>();

        if (family == null) {
            return result;
        }


        // get the font objects from the main list based on family.
        for (FontInfo info : mMainFonts) {
            if (info.families.contains(family)) {
                result.add(info.font[style]);
                break;
            }
        }

        // add all the fallback fonts for the given style
        for (FontInfo info : mFallbackFonts) {
            result.add(info.font[style]);
        }

        return result;
    }


    public synchronized List<Font> getFallbackFonts(int style) {
        List<Font> result = new ArrayList<Font>();
        // add all the fallback fonts
        for (FontInfo info : mFallbackFonts) {
            result.add(info.font[style]);
        }
        return result;
    }


    private final static class FontInfo {
        final Font[] font = new Font[4]; // Matches the 4 type-face styles.
        final Set<String> families;

        FontInfo() {
            families = new HashSet<String>();
        }
    }

    private final static class FontHandler extends DefaultHandler {
        private final String mOsFontsLocation;

        private FontInfo mFontInfo = null;
        private final StringBuilder mBuilder = new StringBuilder();
        private List<FontInfo> mFontList = new ArrayList<FontInfo>();
        private boolean isCompactFont = true;

        private FontHandler(String osFontsLocation) {
            super();
            mOsFontsLocation = osFontsLocation;
        }

        public List<FontInfo> getFontList() {
            return mFontList;
        }

        /* (non-Javadoc)
         * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
         */
        @Override
        public void startElement(String uri, String localName, String name, Attributes attributes)
                throws SAXException {
            if (NODE_FAMILYSET.equals(localName)) {
                mFontList = new ArrayList<FontInfo>();
            } else if (NODE_FAMILY.equals(localName)) {
                if (mFontList != null) {
                    mFontInfo = null;
                }
            } else if (NODE_NAME.equals(localName)) {
                if (mFontList != null && mFontInfo == null) {
                    mFontInfo = new FontInfo();
                }
            } else if (NODE_FILE.equals(localName)) {
                if (mFontList != null && mFontInfo == null) {
                    mFontInfo = new FontInfo();
                }
                if (ATTRIBUTE_VALUE_ELEGANT.equals(attributes.getValue(ATTRIBUTE_VARIANT))) {
                    isCompactFont = false;
                } else {
                    isCompactFont = true;
                }
            }

            mBuilder.setLength(0);

            super.startElement(uri, localName, name, attributes);
        }

        /* (non-Javadoc)
         * @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int)
         */
        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            if (isCompactFont) {
              mBuilder.append(ch, start, length);
            }
        }

        /* (non-Javadoc)
         * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
         */
        @Override
        public void endElement(String uri, String localName, String name) throws SAXException {
            if (NODE_FAMILY.equals(localName)) {
                if (mFontInfo != null) {
                    // if has a normal font file, add to the list
                    if (mFontInfo.font[Typeface.NORMAL] != null) {
                        mFontList.add(mFontInfo);

                        // create missing font styles, order is important.
                        if (mFontInfo.font[Typeface.BOLD_ITALIC] == null) {
                            computeDerivedFont(Typeface.BOLD_ITALIC, DERIVE_BOLD_ITALIC);
                        }
                        if (mFontInfo.font[Typeface.ITALIC] == null) {
                            computeDerivedFont(Typeface.ITALIC, DERIVE_ITALIC);
                        }
                        if (mFontInfo.font[Typeface.BOLD] == null) {
                            computeDerivedFont(Typeface.BOLD, DERIVE_BOLD);
                        }
                    }

                    mFontInfo = null;
                }
            } else if (NODE_NAME.equals(localName)) {
                // handle a new name for an existing Font Info
                if (mFontInfo != null) {
                    String family = trimXmlWhitespaces(mBuilder.toString());
                    mFontInfo.families.add(family);
                }
            } else if (NODE_FILE.equals(localName)) {
                // handle a new file for an existing Font Info
                if (isCompactFont && mFontInfo != null) {
                    String fileName = trimXmlWhitespaces(mBuilder.toString());
                    Font font = getFont(fileName);
                    if (font != null) {
                        if (fileName.endsWith(FONT_SUFFIX_REGULAR)) {
                            mFontInfo.font[Typeface.NORMAL] = font;
                        } else if (fileName.endsWith(FONT_SUFFIX_BOLD)) {
                            mFontInfo.font[Typeface.BOLD] = font;
                        } else if (fileName.endsWith(FONT_SUFFIX_BOLDITALIC)) {
                            mFontInfo.font[Typeface.BOLD_ITALIC] = font;
                        } else if (fileName.endsWith(FONT_SUFFIX_ITALIC)) {
                            mFontInfo.font[Typeface.ITALIC] = font;
                        } else if (fileName.endsWith(FONT_SUFFIX_NONE)) {
                            mFontInfo.font[Typeface.NORMAL] = font;
                        }
                    }
                }
            }
        }

        private Font getFont(String fileName) {
            try {
                File file = new File(mOsFontsLocation, fileName);
                if (file.exists()) {
                    return Font.createFont(Font.TRUETYPE_FONT, file);
                }
            } catch (Exception e) {

            }

            return null;
        }

        private void computeDerivedFont( int toCompute, int[] basedOnList) {
            for (int basedOn : basedOnList) {
                if (mFontInfo.font[basedOn] != null) {
                    mFontInfo.font[toCompute] =
                        mFontInfo.font[basedOn].deriveFont(AWT_STYLES[toCompute]);
                    return;
                }
            }

            // we really shouldn't stop there. This means we don't have a NORMAL font...
            assert false;
        }

        private String trimXmlWhitespaces(String value) {
            if (value == null) {
                return null;
            }

            // look for carriage return and replace all whitespace around it by just 1 space.
            int index;

            while ((index = value.indexOf('\n')) != -1) {
                // look for whitespace on each side
                int left = index - 1;
                while (left >= 0) {
                    if (Character.isWhitespace(value.charAt(left))) {
                        left--;
                    } else {
                        break;
                    }
                }

                int right = index + 1;
                int count = value.length();
                while (right < count) {
                    if (Character.isWhitespace(value.charAt(right))) {
                        right++;
                    } else {
                        break;
                    }
                }

                // remove all between left and right (non inclusive) and replace by a single space.
                String leftString = null;
                if (left >= 0) {
                    leftString = value.substring(0, left + 1);
                }
                String rightString = null;
                if (right < count) {
                    rightString = value.substring(right);
                }

                if (leftString != null) {
                    value = leftString;
                    if (rightString != null) {
                        value += " " + rightString;
                    }
                } else {
                    value = rightString != null ? rightString : "";
                }
            }

            // now we un-escape the string
            int length = value.length();
            char[] buffer = value.toCharArray();

            for (int i = 0 ; i < length ; i++) {
                if (buffer[i] == '\\') {
                    if (buffer[i+1] == 'n') {
                        // replace the char with \n
                        buffer[i+1] = '\n';
                    }

                    // offset the rest of the buffer since we go from 2 to 1 char
                    System.arraycopy(buffer, i+1, buffer, i, length - i - 1);
                    length--;
                }
            }

            return new String(buffer, 0, length);
        }

    }
}
