/*
 * 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;

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.awt.FontFormatException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
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_DEFINITIONS = "fonts.xml";

    private static final String NODE_FONTS = "fonts";
    private static final String NODE_FONT = "font";
    private static final String NODE_NAME = "name";
    private static final String NODE_FALLBACK = "fallback";

    private static final String ATTR_TTF = "ttf";

    private static final String FONT_EXT = ".ttf";

    private static final String[] FONT_STYLE_DEFAULT = { "", "-Regular" };
    private static final String[] FONT_STYLE_BOLD = { "-Bold" };
    private static final String[] FONT_STYLE_ITALIC = { "-Italic" };
    private static final String[] FONT_STYLE_BOLDITALIC = { "-BoldItalic" };

    // list of font style, in the order matching the Typeface Font style
    private static final String[][] FONT_STYLES = {
        FONT_STYLE_DEFAULT,
        FONT_STYLE_BOLD,
        FONT_STYLE_ITALIC,
        FONT_STYLE_BOLDITALIC
    };

    private final Map<String, String> mFamilyToTtf = new HashMap<String, String>();
    private final Map<String, Map<Integer, Font>> mTtfToFontMap =
        new HashMap<String, Map<Integer, Font>>();

    private List<Font> mFallBackFonts = null;

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

            SAXParser parser = parserFactory.newSAXParser();
            File f = new File(fontOsLocation + File.separator + FONTS_DEFINITIONS);

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

            return definitionParser.getFontLoader();
        } 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 FontLoader(List<FontInfo> fontList, List<String> fallBackList) {
        for (FontInfo info : fontList) {
            for (String family : info.families) {
                mFamilyToTtf.put(family, info.ttf);
            }
        }

        ArrayList<Font> list = new ArrayList<Font>();
        for (String path : fallBackList) {
            File f = new File(path + FONT_EXT);
            if (f.isFile()) {
                try {
                    Font font = Font.createFont(Font.TRUETYPE_FONT, f);
                    if (font != null) {
                        list.add(font);
                    }
                } catch (FontFormatException e) {
                    // skip this font name
                } catch (IOException e) {
                    // skip this font name
                }
            }
        }

        mFallBackFonts = Collections.unmodifiableList(list);
    }

    public List<Font> getFallBackFonts() {
        return mFallBackFonts;
    }

    public synchronized Font getFont(String family, int[] style) {
        if (family == null) {
            return null;
        }

        // get the ttf name from the family
        String ttf = mFamilyToTtf.get(family);

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

        // get the font from the ttf
        Map<Integer, Font> styleMap = mTtfToFontMap.get(ttf);

        if (styleMap == null) {
            styleMap = new HashMap<Integer, Font>();
            mTtfToFontMap.put(ttf, styleMap);
        }

        Font f = styleMap.get(style);

        if (f != null) {
            return f;
        }

        // if it doesn't exist, we create it, and we can't, we try with a simpler style
        switch (style[0]) {
            case Typeface.NORMAL:
                f = getFont(ttf, FONT_STYLES[Typeface.NORMAL]);
                break;
            case Typeface.BOLD:
            case Typeface.ITALIC:
                f = getFont(ttf, FONT_STYLES[style[0]]);
                if (f == null) {
                    f = getFont(ttf, FONT_STYLES[Typeface.NORMAL]);
                    style[0] = Typeface.NORMAL;
                }
                break;
            case Typeface.BOLD_ITALIC:
                f = getFont(ttf, FONT_STYLES[style[0]]);
                if (f == null) {
                    f = getFont(ttf, FONT_STYLES[Typeface.BOLD]);
                    if (f != null) {
                        style[0] = Typeface.BOLD;
                    } else {
                        f = getFont(ttf, FONT_STYLES[Typeface.ITALIC]);
                        if (f != null) {
                            style[0] = Typeface.ITALIC;
                        } else {
                            f = getFont(ttf, FONT_STYLES[Typeface.NORMAL]);
                            style[0] = Typeface.NORMAL;
                        }
                    }
                }
                break;
        }

        if (f != null) {
            styleMap.put(style[0], f);
            return f;
        }

        return null;
    }

    private Font getFont(String ttf, String[] fontFileSuffix) {
        for (String suffix : fontFileSuffix) {
            String name = ttf + suffix + FONT_EXT;

            File f = new File(name);
            if (f.isFile()) {
                try {
                    Font font = Font.createFont(Font.TRUETYPE_FONT, f);
                    if (font != null) {
                        return font;
                    }
                } catch (FontFormatException e) {
                    // skip this font name
                } catch (IOException e) {
                    // skip this font name
                }
            }
        }

        return null;
    }

    private final static class FontInfo {
        String ttf;
        final Set<String> families;

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

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

        private FontInfo mFontInfo = null;
        private final StringBuilder mBuilder = new StringBuilder();
        private List<FontInfo> mFontList;
        private List<String> mFallBackList;

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

        FontLoader getFontLoader() {
            return new FontLoader(mFontList, mFallBackList);
        }

        /* (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_FONTS.equals(localName)) {
                mFontList = new ArrayList<FontInfo>();
                mFallBackList = new ArrayList<String>();
            } else if (NODE_FONT.equals(localName)) {
                if (mFontList != null) {
                    String ttf = attributes.getValue(ATTR_TTF);
                    if (ttf != null) {
                        mFontInfo = new FontInfo();
                        mFontInfo.ttf = mOsFontsLocation + ttf;
                        mFontList.add(mFontInfo);
                    }
                }
            } else if (NODE_NAME.equals(localName)) {
                // do nothing, we'll handle the name in the endElement
            } else if (NODE_FALLBACK.equals(localName)) {
                if (mFallBackList != null) {
                    String ttf = attributes.getValue(ATTR_TTF);
                    if (ttf != null) {
                        mFallBackList.add(mOsFontsLocation + ttf);
                    }
                }
            }

            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 {
            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_FONTS.equals(localName)) {
                // top level, do nothing
            } else if (NODE_FONT.equals(localName)) {
                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_FALLBACK.equals(localName)) {
                // nothing to do here.
            }
        }

        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);
        }

    }
}
