/*
 * Copyright (C) 2007 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.unit_tests;

import android.graphics.Bitmap;
import android.sax.Element;
import android.sax.ElementListener;
import android.sax.EndTextElementListener;
import android.sax.RootElement;
import android.sax.StartElementListener;
import android.sax.TextElementListener;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.LargeTest;
import android.test.suitebuilder.annotation.SmallTest;
import android.text.format.Time;
import android.util.Log;
import android.util.Xml;
import com.android.internal.util.XmlUtils;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class SafeSaxTest extends AndroidTestCase {

    private static final String TAG = SafeSaxTest.class.getName();

    private static final String ATOM_NAMESPACE = "http://www.w3.org/2005/Atom";
    private static final String MEDIA_NAMESPACE = "http://search.yahoo.com/mrss/";
    private static final String YOUTUBE_NAMESPACE = "http://gdata.youtube.com/schemas/2007";
    private static final String GDATA_NAMESPACE = "http://schemas.google.com/g/2005";

    private static class ElementCounter implements ElementListener {
        int starts = 0;
        int ends = 0;

        public void start(Attributes attributes) {
            starts++;
        }

        public void end() {
            ends++;
        }
    }

    private static class TextElementCounter implements TextElementListener {
        int starts = 0;
        String bodies = "";

        public void start(Attributes attributes) {
            starts++;
        }

        public void end(String body) {
            this.bodies += body;
        }
    }

    @SmallTest
    public void testListener() throws Exception {
        String xml = "<feed xmlns='http://www.w3.org/2005/Atom'>\n"
                + "<entry>\n"
                + "<id>a</id>\n"
                + "</entry>\n"
                + "<entry>\n"
                + "<id>b</id>\n"
                + "</entry>\n"
                + "</feed>\n";

        RootElement root = new RootElement(ATOM_NAMESPACE, "feed");
        Element entry = root.requireChild(ATOM_NAMESPACE, "entry");
        Element id = entry.requireChild(ATOM_NAMESPACE, "id");

        ElementCounter rootCounter = new ElementCounter();
        ElementCounter entryCounter = new ElementCounter();
        TextElementCounter idCounter = new TextElementCounter();

        root.setElementListener(rootCounter);
        entry.setElementListener(entryCounter);
        id.setTextElementListener(idCounter);

        Xml.parse(xml, root.getContentHandler());

        assertEquals(1, rootCounter.starts);
        assertEquals(1, rootCounter.ends);
        assertEquals(2, entryCounter.starts);
        assertEquals(2, entryCounter.ends);
        assertEquals(2, idCounter.starts);
        assertEquals("ab", idCounter.bodies);
    }

    @SmallTest
    public void testMissingRequiredChild() throws Exception {
        String xml = "<feed></feed>";
        RootElement root = new RootElement("feed");
        root.requireChild("entry");

        try {
            Xml.parse(xml, root.getContentHandler());
            fail("expected exception not thrown");
        } catch (SAXException e) {
            // Expected.
        }
    }

    @SmallTest
    public void testMixedContent() throws Exception {
        String xml = "<feed><entry></entry></feed>";

        RootElement root = new RootElement("feed");
        root.setEndTextElementListener(new EndTextElementListener() {
            public void end(String body) {
            }
        });

        try {
            Xml.parse(xml, root.getContentHandler());
            fail("expected exception not thrown");
        } catch (SAXException e) {
            // Expected.
        }
    }

    @LargeTest
    public void testPerformance() throws Exception {
        InputStream in = mContext.getResources().openRawResource(R.raw.youtube);
        byte[] xmlBytes;
        try {
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int length;
            while ((length = in.read(buffer)) != -1) {
                out.write(buffer, 0, length);
            }
            xmlBytes = out.toByteArray();
        } finally {
            in.close();
        }

        Log.i("***", "File size: " + (xmlBytes.length / 1024) + "k");

        VideoAdapter videoAdapter = new VideoAdapter();
        ContentHandler handler = newContentHandler(videoAdapter);
        for (int i = 0; i < 2; i++) {
            pureSaxTest(new ByteArrayInputStream(xmlBytes));
            saxyModelTest(new ByteArrayInputStream(xmlBytes));
            saxyModelTest(new ByteArrayInputStream(xmlBytes), handler);
        }
    }

    private static void pureSaxTest(InputStream inputStream) throws IOException, SAXException {
        long start = System.currentTimeMillis();
        VideoAdapter videoAdapter = new VideoAdapter();
        Xml.parse(inputStream, Xml.Encoding.UTF_8, new YouTubeContentHandler(videoAdapter));
        long elapsed = System.currentTimeMillis() - start;
        Log.i(TAG, "pure SAX: " + elapsed + "ms");
    }

    private static void saxyModelTest(InputStream inputStream) throws IOException, SAXException {
        long start = System.currentTimeMillis();
        VideoAdapter videoAdapter = new VideoAdapter();
        Xml.parse(inputStream, Xml.Encoding.UTF_8, newContentHandler(videoAdapter));
        long elapsed = System.currentTimeMillis() - start;
        Log.i(TAG, "Saxy Model: " + elapsed + "ms");
    }

    private static void saxyModelTest(InputStream inputStream, ContentHandler contentHandler)
            throws IOException, SAXException {
        long start = System.currentTimeMillis();
        Xml.parse(inputStream, Xml.Encoding.UTF_8, contentHandler);
        long elapsed = System.currentTimeMillis() - start;
        Log.i(TAG, "Saxy Model (preloaded): " + elapsed + "ms");
    }

    private static class VideoAdapter {
        public void addVideo(YouTubeVideo video) {
        }
    }

    private static ContentHandler newContentHandler(VideoAdapter videoAdapter) {
        return new HandlerFactory().newContentHandler(videoAdapter);
    }

    private static class HandlerFactory {
        YouTubeVideo video;

        public ContentHandler newContentHandler(VideoAdapter videoAdapter) {
            RootElement root = new RootElement(ATOM_NAMESPACE, "feed");

            final VideoListener videoListener = new VideoListener(videoAdapter);

            Element entry = root.getChild(ATOM_NAMESPACE, "entry");

            entry.setElementListener(videoListener);

            entry.getChild(ATOM_NAMESPACE, "id")
                    .setEndTextElementListener(new EndTextElementListener() {
                        public void end(String body) {
                            video.videoId = body;
                        }
                    });

            entry.getChild(ATOM_NAMESPACE, "published")
                    .setEndTextElementListener(new EndTextElementListener() {
                        public void end(String body) {
                            // TODO(tomtaylor): programmatically get the timezone
                            video.dateAdded = new Time(Time.TIMEZONE_UTC);
                            video.dateAdded.parse3339(body);
                        }
                    });

            Element author = entry.getChild(ATOM_NAMESPACE, "author");
            author.getChild(ATOM_NAMESPACE, "name")
                    .setEndTextElementListener(new EndTextElementListener() {
                        public void end(String body) {
                            video.authorName = body;
                        }
                    });

            Element mediaGroup = entry.getChild(MEDIA_NAMESPACE, "group");

            mediaGroup.getChild(MEDIA_NAMESPACE, "thumbnail")
                    .setStartElementListener(new StartElementListener() {
                        public void start(Attributes attributes) {
                            String url = attributes.getValue("", "url");
                            if (video.thumbnailUrl == null && url.length() > 0) {
                                video.thumbnailUrl = url;
                            }
                        }
                    });

            mediaGroup.getChild(MEDIA_NAMESPACE, "content")
                    .setStartElementListener(new StartElementListener() {
                        public void start(Attributes attributes) {
                            String url = attributes.getValue("", "url");
                            if (url != null) {
                                video.videoUrl = url;
                            }
                        }
                    });

            mediaGroup.getChild(MEDIA_NAMESPACE, "player")
                    .setStartElementListener(new StartElementListener() {
                        public void start(Attributes attributes) {
                            String url = attributes.getValue("", "url");
                            if (url != null) {
                                video.playbackUrl = url;
                            }
                        }
                    });

            mediaGroup.getChild(MEDIA_NAMESPACE, "title")
                    .setEndTextElementListener(new EndTextElementListener() {
                        public void end(String body) {
                            video.title = body;
                        }
                    });

            mediaGroup.getChild(MEDIA_NAMESPACE, "category")
                    .setEndTextElementListener(new EndTextElementListener() {
                        public void end(String body) {
                            video.category = body;
                        }
                    });

            mediaGroup.getChild(MEDIA_NAMESPACE, "description")
                    .setEndTextElementListener(new EndTextElementListener() {
                        public void end(String body) {
                            video.description = body;
                        }
                    });

            mediaGroup.getChild(MEDIA_NAMESPACE, "keywords")
                    .setEndTextElementListener(new EndTextElementListener() {
                        public void end(String body) {
                            video.tags = body;
                        }
                    });

            mediaGroup.getChild(YOUTUBE_NAMESPACE, "duration")
                    .setStartElementListener(new StartElementListener() {
                        public void start(Attributes attributes) {
                            String seconds = attributes.getValue("", "seconds");
                            video.lengthInSeconds
                                    = XmlUtils.convertValueToInt(seconds, 0);
                        }
                    });

            mediaGroup.getChild(YOUTUBE_NAMESPACE, "statistics")
                    .setStartElementListener(new StartElementListener() {
                        public void start(Attributes attributes) {
                            String viewCount = attributes.getValue("", "viewCount");
                            video.viewCount
                                    = XmlUtils.convertValueToInt(viewCount, 0);
                        }
                    });

            entry.getChild(GDATA_NAMESPACE, "rating")
                    .setStartElementListener(new StartElementListener() {
                        public void start(Attributes attributes) {
                            String average = attributes.getValue("", "average");
                            video.rating = average == null
                                    ? 0.0f : Float.parseFloat(average);
                        }
                    });

            return root.getContentHandler();
        }

        class VideoListener implements ElementListener {

            final VideoAdapter videoAdapter;

            public VideoListener(VideoAdapter videoAdapter) {
                this.videoAdapter = videoAdapter;
            }

            public void start(Attributes attributes) {
                video = new YouTubeVideo();
            }

            public void end() {
                videoAdapter.addVideo(video);
                video = null;
            }
        }
    }

    private static class YouTubeContentHandler extends DefaultHandler {

        final VideoAdapter videoAdapter;

        YouTubeVideo video = null;
        StringBuilder builder = null;

        public YouTubeContentHandler(VideoAdapter videoAdapter) {
            this.videoAdapter = videoAdapter;
        }

        @Override
        public void startElement(String uri, String localName, String qName,
                Attributes attributes) throws SAXException {
            if (uri.equals(ATOM_NAMESPACE)) {
                if (localName.equals("entry")) {
                    video = new YouTubeVideo();
                    return;
                }

                if (video == null) {
                    return;
                }

                if (!localName.equals("id")
                        && !localName.equals("published")
                        && !localName.equals("name")) {
                    return;
                }
                this.builder = new StringBuilder();
                return;

            }

            if (video == null) {
                return;
            }

            if (uri.equals(MEDIA_NAMESPACE)) {
                if (localName.equals("thumbnail")) {
                    String url = attributes.getValue("", "url");
                    if (video.thumbnailUrl == null && url.length() > 0) {
                        video.thumbnailUrl = url;
                    }
                    return;
                }

                if (localName.equals("content")) {
                    String url = attributes.getValue("", "url");
                    if (url != null) {
                        video.videoUrl = url;
                    }
                    return;
                }

                if (localName.equals("player")) {
                    String url = attributes.getValue("", "url");
                    if (url != null) {
                        video.playbackUrl = url;
                    }
                    return;
                }

                if (localName.equals("title")
                        || localName.equals("category")
                        || localName.equals("description")
                        || localName.equals("keywords")) {
                    this.builder = new StringBuilder();
                    return;
                }

                return;
            }

            if (uri.equals(YOUTUBE_NAMESPACE)) {
                if (localName.equals("duration")) {
                    video.lengthInSeconds = XmlUtils.convertValueToInt(
                            attributes.getValue("", "seconds"), 0);
                    return;
                }

                if (localName.equals("statistics")) {
                    video.viewCount = XmlUtils.convertValueToInt(
                            attributes.getValue("", "viewCount"), 0);
                    return;
                }

                return;
            }

            if (uri.equals(GDATA_NAMESPACE)) {
                if (localName.equals("rating")) {
                    String average = attributes.getValue("", "average");
                    video.rating = average == null
                            ? 0.0f : Float.parseFloat(average);
                }
            }
        }

        @Override
        public void characters(char text[], int start, int length)
                throws SAXException {
            if (builder != null) {
                builder.append(text, start, length);
            }
        }

        String takeText() {
            try {
                return builder.toString();
            } finally {
                builder = null;
            }
        }

        @Override
        public void endElement(String uri, String localName, String qName)
                throws SAXException {
            if (video == null) {
                return;
            }

            if (uri.equals(ATOM_NAMESPACE)) {
                if (localName.equals("published")) {
                    // TODO(tomtaylor): programmatically get the timezone
                    video.dateAdded = new Time(Time.TIMEZONE_UTC);
                    video.dateAdded.parse3339(takeText());
                    return;
                }

                if (localName.equals("name")) {
                    video.authorName = takeText();
                    return;
                }

                if (localName.equals("id")) {
                    video.videoId = takeText();
                    return;
                }

                if (localName.equals("entry")) {
                    // Add the video!
                    videoAdapter.addVideo(video);
                    video = null;
                    return;
                }

                return;
            }

            if (uri.equals(MEDIA_NAMESPACE)) {
                if (localName.equals("description")) {
                    video.description = takeText();
                    return;
                }

                if (localName.equals("keywords")) {
                    video.tags = takeText();
                    return;
                }

                if (localName.equals("category")) {
                    video.category = takeText();
                    return;
                }

                if (localName.equals("title")) {
                    video.title = takeText();
                }
            }
        }
    }

    private static class YouTubeVideo {
        public String videoId;     // the id used to lookup on YouTube
        public String videoUrl;       // the url to play the video
        public String playbackUrl;    // the url to share for users to play video
        public String thumbnailUrl;   // the url of the thumbnail image
        public String title;
        public Bitmap bitmap;      // cached bitmap of the thumbnail
        public int lengthInSeconds;
        public int viewCount;      // number of times the video has been viewed
        public float rating;       // ranges from 0.0 to 5.0
        public Boolean triedToLoadThumbnail;
        public String authorName;
        public Time dateAdded;
        public String category;
        public String tags;
        public String description;
    }
}

