/*
 *
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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 org.apache.royale.swf.io;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Helper class generally used by SWFWriterAndSizeReporter to collect size
 * related metrics for a given SWF and to generate an optional XML report of the
 * summarized data.
 */
public class SizeReport
{
    private int size;
    private int currentFrame = 1;
    private int currentFrameSize;
    private int compressedSize;
    private Map<Integer, String> symbols;
    private Map<Integer, DefinitionData> definitionTypes;
    private String prevFrameLabel = null;

    // DefinitionEntry Types
    public static final int BINARY = 0;
    public static final int BITMAP = 1;
    public static final int FONT = 2;
    public static final int FRAME = 3;
    public static final int FRAME_DATA = 4;
    public static final int HEADER_DATA = 5;
    public static final int SCRIPT = 6;
    public static final int SHAPE = 7;
    public static final int SOUND = 8;
    public static final int SPRITE = 9;
    public static final int VIDEO = 10;

    public SizeReport()
    {
        symbols = new HashMap<Integer, String>();
        definitionTypes = new HashMap<Integer, DefinitionData>();
        registerDefinitionType(BINARY, "binaryData", "data",
                "SWF, Pixel Bender, or other miscellaneous embed data.");
        registerDefinitionType(BITMAP, "bitmaps", "bitmap",
                "defineBits, definebitsJPEG2/3/4, or defineBitsLossless/2");
        registerDefinitionType(FONT, "fonts", "font",
                "defineFont/2/3/4.");
        registerDefinitionType(FRAME, "frames", "frame",
                "Cumulative frame size summary.");
        registerDefinitionType(FRAME_DATA, "frameData", "tag",
                "Additional frame tags (symbolClass, exportAssets, showFrame, etc).");
        registerDefinitionType(HEADER_DATA, "headerData", "data",
                "Header data (SWF attributes, product info, markers, etc.)");
        registerDefinitionType(SCRIPT, "actionScript", "abc",
                "Actionscript code and constant data.");
        registerDefinitionType(SHAPE, "shapes", "shape",
                "defineShape/2/3/4.");
        registerDefinitionType(SOUND, "sounds", "sound",
                "defineSound.");
        registerDefinitionType(SPRITE, "sprites", "sprite",
                "defineSprite.");
        registerDefinitionType(VIDEO, "videos", "video",
                "defineVideoStream.");
    }

    public String generate()
    {
        StringBuilder buffer = new StringBuilder(2048);
        buffer.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
        buffer.append("<report>\n");
        {
            // Cumulative Size
            buffer.append("  <swf size=\"");
            buffer.append(size);
            buffer.append("\" compressedSize=\"");
            buffer.append(compressedSize);
            buffer.append("\">\n");

            dumpDefinitionType(buffer, HEADER_DATA, "    ");
            dumpDefinitionType(buffer, FRAME, "    ");
            dumpDefinitionType(buffer, SCRIPT, "    ");
            dumpDefinitionType(buffer, FONT, "    ");
            dumpDefinitionType(buffer, SPRITE, "    ");
            dumpDefinitionType(buffer, SHAPE, "    ");
            dumpDefinitionType(buffer, BITMAP, "    ");
            dumpDefinitionType(buffer, SOUND, "    ");
            dumpDefinitionType(buffer, VIDEO, "    ");
            dumpDefinitionType(buffer, BINARY, "    ");
            dumpDefinitionType(buffer, FRAME_DATA, "    ");

            buffer.append("  </swf>\n");
        }
        buffer.append("</report>");
        return buffer.toString();
    }

    public void setSize(int value)
    {
        size = value;
    }

    public void setCompressedSize(int value)
    {
        compressedSize = value;
    }

    private void dumpDefinitionType(StringBuilder buffer, int type, String indent)
    {
        DefinitionData data = definitionTypes.get(type);
        List<DefinitionEntry> definitions = data.definitions;

        // Sort definitions largest to smallest. Sort frames in frame order.
        Comparator<DefinitionEntry> comparator = type == FRAME ? new FrameComparator() :
                new DefinitionComparator();
        Collections.sort(definitions, comparator);

        if (definitions.size() > 0)
        {
            buffer.append("\n" + indent + "<!-- " + data.description + " -->\n");
            buffer.append(indent + "<" + data.pluralMoniker + " ");
            buffer.append("totalSize=\"");
            buffer.append(data.totalSize);
            buffer.append("\">\n");
            for (DefinitionEntry n : definitions)
            {
                buffer.append(indent + "  <" + data.singularMoniker + " ");
                String name = (type == FRAME || type == SCRIPT) ? n.stringData : symbols.get(n.id);
                if (name != null)
                {
                    buffer.append("name=\"");
                    buffer.append(name);
                    buffer.append("\" ");
                }
                if (type == FONT && n.stringData != null)
                {
                    buffer.append("fontName=\"");
                    buffer.append(n.stringData);
                    buffer.append("\" ");
                }
                else if ((type == HEADER_DATA || type == FRAME_DATA) && n.stringData != null)
                {
                    buffer.append("type=\"");
                    buffer.append(n.stringData);
                    buffer.append("\" ");
                }
                buffer.append("size=\"");
                buffer.append(n.size);

                if (n.frame != -1 && type != HEADER_DATA)
                {
                    buffer.append("\" ");
                    buffer.append("frame=\"");
                    buffer.append(n.frame);
                }

                buffer.append("\"/>\n");
            }
            buffer.append(indent + "</" + data.pluralMoniker + ">\n");
        }
    }

    public void startEntry(int type, int startOffset, int id, String stringData)
    {
        if (type == FRAME)
        {
            prevFrameLabel = stringData;
        }
        else
        {
            DefinitionData data = definitionTypes.get(type);
            DefinitionEntry entry = new DefinitionEntry();
            entry.stringData = stringData;
            entry.size = startOffset;
            entry.id = id;
            entry.frame = currentFrame;
            data.definitions.add(entry);
        }
    }

    public void endEntry(int type, int endOffset, String stringData)
    {
        if (type == FRAME)
        {
            DefinitionData data = definitionTypes.get(type);
            DefinitionEntry entry = new DefinitionEntry();
            entry.size = currentFrameSize;
            entry.stringData = prevFrameLabel;
            entry.frame = currentFrame;
            data.definitions.add(entry);
            data.totalSize += entry.size;
            currentFrame++;
            currentFrameSize = 0; // reset the frame size
        }
        else
        {
            DefinitionData data = definitionTypes.get(type);
            DefinitionEntry entry = data.definitions.get(data.definitions.size() - 1);
            entry.size = endOffset - entry.size;
            data.totalSize += entry.size;
            currentFrameSize += entry.size;
        }
    }

    public void addEntry(int type, int id, int size, String stringData)
    {
        DefinitionData data = definitionTypes.get(type);
        DefinitionEntry entry = new DefinitionEntry();
        data.totalSize += size;
        entry.stringData = stringData;
        entry.size = size;
        entry.id = id;
        entry.frame = currentFrame;
        data.definitions.add(entry);
        currentFrameSize += entry.size;
    }

    public void startEntry(int type, int startOffset, int id)
    {
        startEntry(type, startOffset, id, null);
    }

    public void endEntry(int type, int endOffset)
    {
        endEntry(type, endOffset, null);
    }

    public void addEntry(int type, int id, int size)
    {
        addEntry(type, id, size, null);
    }

    public void addSymbol(String name, int id)
    {
        symbols.put(id, name);
    }

    private void registerDefinitionType(int type, String pluralMoniker, String singularMoniker, String description)
    {
        definitionTypes.put(type, new DefinitionData(pluralMoniker, singularMoniker, description));
    }

    // Private helper classes used to keep track of each definition by type as
    // well as cumulative size for each type.

    private class DefinitionEntry
    {
        public Integer size;
        public Integer id;
        public String stringData;
        public Integer frame = -1;
    }

    private class DefinitionData
    {
        public DefinitionData(String pluralMoniker, String singularMoniker, String description)
        {
            this.singularMoniker = singularMoniker;
            this.pluralMoniker = pluralMoniker;
            this.description = description;
            definitions = new ArrayList<DefinitionEntry>();
        }

        public List<DefinitionEntry> definitions;
        public int totalSize;
        public String singularMoniker;
        public String pluralMoniker;
        public String description;
    }

    private class DefinitionComparator implements Comparator<DefinitionEntry>
    {
        @Override
        public final int compare(DefinitionEntry a, DefinitionEntry b)
        {
            // Largest to smallest.
            return -(a.size.compareTo(b.size));
        }
    }

    private class FrameComparator implements Comparator<DefinitionEntry>
    {
        @Override
        public final int compare(DefinitionEntry a, DefinitionEntry b)
        {
            // frame 1 is first.
            return a.frame.compareTo(b.frame);
        }
    }
}
