/*
 *
 *  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.compiler.internal.embedding.transcoders;

import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.apache.royale.compiler.caches.ISWFCache;
import org.apache.royale.compiler.common.ISourceLocation;
import org.apache.royale.compiler.constants.IASKeywordConstants;
import org.apache.royale.compiler.constants.INamespaceConstants;
import org.apache.royale.compiler.internal.caches.CacheStoreKeyBase;
import org.apache.royale.compiler.internal.caches.SWFCache;
import org.apache.royale.compiler.embedding.EmbedAttribute;
import org.apache.royale.compiler.internal.embedding.EmbedData;
import org.apache.royale.compiler.internal.parsing.as.ASToken;
import org.apache.royale.compiler.internal.parsing.as.ASTokenTypes;
import org.apache.royale.compiler.internal.semantics.PostProcessStep;
import org.apache.royale.compiler.internal.targets.RoyaleFontInfo;
import org.apache.royale.compiler.internal.targets.TagSorter;
import org.apache.royale.compiler.internal.tree.as.BinaryOperatorNodeBase;
import org.apache.royale.compiler.internal.tree.as.BlockNode;
import org.apache.royale.compiler.internal.tree.as.ClassNode;
import org.apache.royale.compiler.internal.tree.as.ConditionalNode;
import org.apache.royale.compiler.internal.tree.as.ContainerNode;
import org.apache.royale.compiler.internal.tree.as.FileNode;
import org.apache.royale.compiler.internal.tree.as.FunctionCallNode;
import org.apache.royale.compiler.internal.tree.as.FunctionNode;
import org.apache.royale.compiler.internal.tree.as.GetterNode;
import org.apache.royale.compiler.internal.tree.as.IdentifierNode;
import org.apache.royale.compiler.internal.tree.as.IfNode;
import org.apache.royale.compiler.internal.tree.as.ImportNode;
import org.apache.royale.compiler.internal.tree.as.LanguageIdentifierNode;
import org.apache.royale.compiler.internal.tree.as.LiteralNode;
import org.apache.royale.compiler.internal.tree.as.ModifierNode;
import org.apache.royale.compiler.internal.tree.as.NamespaceIdentifierNode;
import org.apache.royale.compiler.internal.tree.as.NumericLiteralNode;
import org.apache.royale.compiler.internal.tree.as.PackageNode;
import org.apache.royale.compiler.internal.tree.as.ReturnNode;
import org.apache.royale.compiler.internal.tree.as.ScopedBlockNode;
import org.apache.royale.compiler.internal.tree.as.VariableNode;
import org.apache.royale.compiler.internal.workspaces.Workspace;
import org.apache.royale.compiler.problems.EmbedAS2TagsModifiedProblem;
import org.apache.royale.compiler.problems.EmbedBadScalingGridTargetProblem;
import org.apache.royale.compiler.problems.EmbedMissingSymbolProblem;
import org.apache.royale.compiler.problems.EmbedMovieScalingNoSymbolProblem;
import org.apache.royale.compiler.problems.ICompilerProblem;
import org.apache.royale.compiler.tree.as.ILiteralNode.LiteralType;
import org.apache.royale.swc.ISWC;
import org.apache.royale.swc.ISWCManager;
import org.apache.royale.swf.ISWF;
import org.apache.royale.swf.ISWFConstants;
import org.apache.royale.swf.io.SWFReader;
import org.apache.royale.swf.tags.DefineBitsLosslessTag;
import org.apache.royale.swf.tags.DefineBitsTag;
import org.apache.royale.swf.tags.DefineButtonTag;
import org.apache.royale.swf.tags.DefineFont2Tag;
import org.apache.royale.swf.tags.DefineFont4Tag;
import org.apache.royale.swf.tags.DefineFontTag;
import org.apache.royale.swf.tags.DefineScalingGridTag;
import org.apache.royale.swf.tags.DefineSoundTag;
import org.apache.royale.swf.tags.DefineSpriteTag;
import org.apache.royale.swf.tags.DefineTextTag;
import org.apache.royale.swf.tags.ExportAssetsTag;
import org.apache.royale.swf.tags.ICharacterTag;
import org.apache.royale.swf.tags.ITag;
import org.apache.royale.swf.tags.PlaceObject2Tag;
import org.apache.royale.swf.tags.SymbolClassTag;
import org.apache.royale.swf.types.Rect;

public class MovieTranscoder extends ScalableTranscoder
{
    /**
     * Constructor.
     * 
     * @param data The embedding data.
     * @param workspace The workspace.
     */
    public MovieTranscoder(EmbedData data, Workspace workspace)
    {
        super(data, workspace);
        this.symbol = null;
        this.symbolTag = null;
        this.royaleFontInfo = null;
        this.swfWidth = 0;
        this.swfHeight = 0;
    }

    private String symbol;
    private ICharacterTag symbolTag;
    private RoyaleFontInfo royaleFontInfo;
    private int swfWidth;
    private int swfHeight;

    @Override
    public boolean analyze(ISourceLocation location, Collection<ICompilerProblem> problems)
    {
        boolean result = super.analyze(location, problems);
        if (!result)
            return false;

        SWFReader swfReader = getSWFReader(problems);
        if (swfReader == null)
            return false;

        ISWF swf = swfReader.getSWF();
        if (swf == null)
            return false;

        if (symbol == null)
        {
            baseClassQName = CORE_PACKAGE + ".MovieClipLoaderAsset";

            if (scaling)
            {
                problems.add(new EmbedMovieScalingNoSymbolProblem(location));
            }
        }
        else
        {
            symbolTag = getSWFTag(swfReader, symbol, problems);
            if (symbolTag == null)
            {
                problems.add(new EmbedMissingSymbolProblem(location, source, symbol));
                return false;
            }

            if (scaling)
            {
                if (!(symbolTag instanceof DefineSpriteTag))
                {
                    problems.add(new EmbedBadScalingGridTargetProblem(location, symbol));
                    return false;
                }
            }

            if (swf.getFrameCount() > 1)
                baseClassQName = CORE_PACKAGE + ".MovieClipLoaderAsset";
            else
                baseClassQName = getAssociatedClass(symbolTag);

            if (symbolTag instanceof DefineFont2Tag)
            {
                royaleFontInfo = new RoyaleFontInfo(((DefineFont2Tag)symbolTag).isFontFlagsBold(), ((DefineFont2Tag)symbolTag).isFontFlagsItalic());
            }
            else if (symbolTag instanceof DefineFont4Tag)
            {
                royaleFontInfo = new RoyaleFontInfo(((DefineFont4Tag)symbolTag).isFontFlagsBold(), ((DefineFont4Tag)symbolTag).isFontFlagsItalic());                
            }
        }

        Rect swfSize = swf.getFrameSize();
        if (swfSize != null)
        {
            swfWidth =  swfSize.getWidth() / ISWFConstants.TWIPS_PER_PIXEL;
            swfHeight = swfSize.getHeight() / ISWFConstants.TWIPS_PER_PIXEL;
        }
        else
        {
            result = false;
        }

        return result;
    }

    @Override
    protected boolean setAttribute(EmbedAttribute attribute)
    {
        boolean isSupported = true;
        switch (attribute)
        {
            case SYMBOL:
                symbol = (String)data.getAttribute(EmbedAttribute.SYMBOL);
                break;
            default:
                isSupported = super.setAttribute(attribute);
        }

        return isSupported;
    }

    @Override
    protected Map<String, ICharacterTag> doTranscode(Collection<ITag> tags, Collection<ICompilerProblem> problems)
    {
        ICharacterTag assetTag = null;
        String symbolName;
        if (symbolTag != null)
        {
            symbolName = data.getQName();
            Set<ITag> sortedTags = new LinkedHashSet<ITag>();
            sortedTags.addAll(TagSorter.sortFullGraph(Collections.<ITag>singletonList(symbolTag)));

            if (scaling)
            {
                tags.addAll(sortedTags);
                assetTag = scaleExistingSprite((DefineSpriteTag)symbolTag, tags, problems);
            }
            else
            {
                sortedTags.remove(symbolTag);
                tags.addAll(sortedTags);
                assetTag = symbolTag;
            }
        }
        else
        {
            symbolName = data.getQName() + byteArrayNamePostfix;

            assetTag = buildBinaryDataTag(problems);
            if (assetTag == null)
                return null;
        }

        Map<String, ICharacterTag> symbolTags = Collections.singletonMap(symbolName, assetTag);
        return symbolTags;
    }

    @Override
    public FileNode buildAST(Collection<ICompilerProblem> problems, String filename)
    {
        // when a symbol has been specified, no need to generate the wrapper class
        if (symbolTag != null)
        {
            return super.buildAST(problems, filename);
        }

        FileNode fileNode = new FileNode(workspace, filename);
        PackageNode packageNode = new PackageNode(new IdentifierNode(""), null);
        fileNode.addItem(packageNode);

        ScopedBlockNode packageContents = packageNode.getScopedNode();
        ImportNode importNode = ImportNode.buildImportNode(getBaseClassQName());
        packageContents.addItem(importNode);
        importNode = ImportNode.buildImportNode("mx.core.ByteArrayAsset");
        packageContents.addItem(importNode);
        importNode = ImportNode.buildImportNode("flash.utils.ByteArray");
        packageContents.addItem(importNode);

        // generate the byte array class
        String byteArrayClassName = data.getQName() + byteArrayNamePostfix;
        ClassNode classNodeByteArray = new ClassNode(new IdentifierNode(byteArrayClassName));
        classNodeByteArray.setBaseClass(new IdentifierNode("ByteArrayAsset"));
        classNodeByteArray.setNamespace(new NamespaceIdentifierNode(INamespaceConstants.public_));
        packageContents.addItem(classNodeByteArray);

        // generate the movie class
        String movieClassName = data.getQName();
        ClassNode classNodeMovie = new ClassNode(new IdentifierNode(movieClassName));
        classNodeMovie.setBaseClass(new IdentifierNode(getBaseClassName()));
        classNodeMovie.setNamespace(new NamespaceIdentifierNode(INamespaceConstants.public_));
        packageContents.addItem(classNodeMovie);
        ScopedBlockNode classNodeMovieContents = classNodeMovie.getScopedNode();

        // generate: private static var bytes:ByteArray = null;
        VariableNode variableNodeBytes = new VariableNode(new IdentifierNode("bytes"));
        variableNodeBytes.setNamespace(new NamespaceIdentifierNode(INamespaceConstants.private_));
        variableNodeBytes.addModifier(new ModifierNode(IASKeywordConstants.STATIC));
        variableNodeBytes.setType(null, new IdentifierNode("ByteArray"));
        ASToken assignToken = new ASToken(ASTokenTypes.TOKEN_OPERATOR_ASSIGNMENT, -1, -1, -1, -1, "=");
        ASToken nullToken = new ASToken(ASTokenTypes.TOKEN_KEYWORD_NULL, -1, -1, -1, -1, IASKeywordConstants.NULL);
        LiteralNode nullNode = new LiteralNode(LiteralType.NULL, nullToken);
        variableNodeBytes.setAssignedValue(assignToken, nullNode);
        classNodeMovieContents.addItem(variableNodeBytes);

        // build the constructor
        IdentifierNode constructorNameNode = new IdentifierNode(movieClassName);
        constructorNameNode.setReferenceValue(classNodeMovie.getDefinition());
        FunctionNode constructorNode = new FunctionNode(null, constructorNameNode);
        constructorNode.setNamespace(new NamespaceIdentifierNode(INamespaceConstants.public_));
        ScopedBlockNode constructorContents = constructorNode.getScopedNode();

        // generate: super();
        FunctionCallNode superCall = new FunctionCallNode(LanguageIdentifierNode.buildSuper());
        constructorContents.addItem(superCall);

        // generate: initialWidth = $swfWidth;
        LiteralNode widthNode = new NumericLiteralNode(Integer.toString(swfWidth));
        BinaryOperatorNodeBase assignmentWidth = BinaryOperatorNodeBase.create(assignToken, new IdentifierNode("initialWidth"), widthNode);
        constructorContents.addItem(assignmentWidth);

        // generate: initialHeight = $swfHeight;
        LiteralNode heightNode = new NumericLiteralNode(Integer.toString(swfHeight));
        BinaryOperatorNodeBase assignmentHeight = BinaryOperatorNodeBase.create(assignToken, new IdentifierNode("initialHeight"), heightNode);
        constructorContents.addItem(assignmentHeight);

        classNodeMovieContents.addItem(constructorNode);

        // build the movieClipData() getter
        GetterNode movieClipDataGetterNode = new GetterNode(null, null, new IdentifierNode("movieClipData"));
        movieClipDataGetterNode.addModifier(new ModifierNode(IASKeywordConstants.OVERRIDE));
        movieClipDataGetterNode.setNamespace(new NamespaceIdentifierNode(INamespaceConstants.public_));
        movieClipDataGetterNode.setType(null, new IdentifierNode("ByteArray"));
        ScopedBlockNode movieClipDataContents = movieClipDataGetterNode.getScopedNode();

        // generate: if (bytes == null)
        ASToken compareToken = new ASToken(ASTokenTypes.TOKEN_OPERATOR_EQUAL, -1, -1, -1, -1, "==");
        BinaryOperatorNodeBase nullCheck = BinaryOperatorNodeBase.create(compareToken, new IdentifierNode("bytes"), new LiteralNode(LiteralType.NULL, nullToken));
        IfNode ifStmt = new IfNode(null);
        ConditionalNode cNode = new ConditionalNode(null);
        cNode.setConditionalExpression(nullCheck);
        ifStmt.addBranch(cNode);
        movieClipDataContents.addItem(ifStmt);
        BlockNode ifContents = cNode.getContentsNode();

        // generate: bytes = ByteArray(new $assetByteArray());
        ASToken newToken = new ASToken(ASTokenTypes.TOKEN_KEYWORD_NEW, -1, -1, -1, -1, IASKeywordConstants.NEW);
        FunctionCallNode newBytes = new FunctionCallNode(newToken, new IdentifierNode(byteArrayClassName));
        FunctionCallNode byteArrayCall = new FunctionCallNode(new IdentifierNode("ByteArray"));
        ContainerNode args = byteArrayCall.getArgumentsNode();
        args.addItem(newBytes);
        BinaryOperatorNodeBase assignmentBytes = BinaryOperatorNodeBase.create(assignToken, new IdentifierNode("bytes"), byteArrayCall);
        ifContents.addItem(assignmentBytes);

        // generate: return bytes;
        ReturnNode returnStmt = new ReturnNode(null);
        returnStmt.setStatementExpression(new IdentifierNode("bytes"));
        movieClipDataContents.addItem(returnStmt);

        classNodeMovieContents.addItem(movieClipDataGetterNode);

        fileNode.runPostProcess(EnumSet.of(PostProcessStep.POPULATE_SCOPE));

        return fileNode;
    }

    /**
     * Get the symbol name of the embedded SWF, or null if the entire SWF is
     * being embedded.
     * 
     * @return the symbol name, or null if none specified.
     */
    public String getSymbol()
    {
        return symbol;
    }

    /**
     * Get the font info if the SWF being embedded is a font, or null if not
     * a font.
     * 
     * @return RoyaleFontInfo for the font, or null.
     */
    public RoyaleFontInfo getRoyaleFontInfo()
    {
        return royaleFontInfo;
    }

    private SWFReader getSWFReader(Collection<ICompilerProblem> problems)
    {
        
        CacheStoreKeyBase cacheKey;
        ISWCManager swcManager = workspace.getSWCManager();
        if (super.swcSource == null)
            cacheKey = SWFCache.createKey(source);
        else
        {
            final ISWC swc = swcManager.get(new File(super.swcSource.getContainingSWCPath()));
            cacheKey = SWFCache.createKey(swc, source);
        }
        SWFCache swfCache = (SWFCache)workspace.getSWCManager().getSWFCache();
        SWFReader swfReader = (SWFReader)swfCache.get(cacheKey);
        return swfReader;
    }

    private ICharacterTag getSWFTag(SWFReader swfReader, String tagName, Collection<ICompilerProblem> problems)
    {
        ICharacterTag characterTag = null;
        for (ITag tag : swfReader)
        {
            if (tag instanceof SymbolClassTag)
            {
                SymbolClassTag symbolClassTag = (SymbolClassTag)tag;
                characterTag = symbolClassTag.getSymbol(tagName);
                if (characterTag != null)
                    break;
            }
            else if (tag instanceof ExportAssetsTag)
            {
                ExportAssetsTag exportAssetsTag = (ExportAssetsTag)tag;
                characterTag = exportAssetsTag.getCharacterTagByName(tagName);
                if (characterTag != null)
                    break;
            }
        }

        if (characterTag != null)
        {
            boolean tagsModified = modifyTagsForEmbedding(characterTag);
            if (tagsModified)
            {
                problems.add(new EmbedAS2TagsModifiedProblem(symbol));                
            }
        }

        return characterTag;
    }

    /**
     * Certain AS2 tags and actions shouldn't be copied over into
     * AS3 tags, so filter out any unvalid tags/actions here.
     * 
     * @param ICharacterTag tag
     * @return true if tag has been modified
     */
    private boolean modifyTagsForEmbedding(ICharacterTag tag)
    {
        boolean tagsModified = false;
        if (tag instanceof DefineSpriteTag)
        {
            DefineSpriteTag spriteTag = (DefineSpriteTag)tag;
            for (ITag controlTag : spriteTag.getControlTags())
            {
                if (controlTag instanceof PlaceObject2Tag)
                {
                    PlaceObject2Tag placeObject = (PlaceObject2Tag)controlTag;
                    if (placeObject.isHasClipActions())
                    {
                        placeObject.setCharacter(null);
                        tagsModified = true;
                    }
                    if (placeObject.isHasCharacter())
                    {
                        tagsModified |= modifyTagsForEmbedding(placeObject.getCharacter());
                    }
                }
            }
        }
        else if (tag instanceof DefineButtonTag)
        {
            DefineButtonTag buttonTag = (DefineButtonTag)tag;
            byte[] actions = buttonTag.getActions();
            if (actions != null && actions.length > 0)
            {
                buttonTag.setActions(null);
                tagsModified = true;
            }
        }

        return tagsModified;
    }

    private String getAssociatedClass(ICharacterTag tag)
    {
        // default to SpriteAsset
        String associatedClass = CORE_PACKAGE + ".SpriteAsset";

        if (tag instanceof DefineButtonTag)
            associatedClass = CORE_PACKAGE + ".ButtonAsset";
        else if (tag instanceof DefineFontTag || tag instanceof DefineFont4Tag)
            associatedClass = CORE_PACKAGE + ".FontAsset";
        else if (tag instanceof DefineTextTag)
            associatedClass = CORE_PACKAGE + ".TextFieldAsset";
        else if (tag instanceof DefineSoundTag)
            associatedClass = CORE_PACKAGE + ".SoundAsset";
        else if (tag instanceof DefineBitsTag || tag instanceof DefineBitsLosslessTag)
            associatedClass = CORE_PACKAGE + ".BitmapAsset";
        else if (tag instanceof DefineSpriteTag)
            associatedClass = CORE_PACKAGE + ".SpriteAsset";

        return associatedClass;
    }

    private DefineSpriteTag scaleExistingSprite(DefineSpriteTag sprite, Collection<ITag> tags, Collection<ICompilerProblem> problems)
    {
        DefineScalingGridTag scalingGrid = buildScalingGrid();
        return buildSprite(sprite.getControlTags(), sprite.getFrameCount(), scalingGrid, tags);
    }

    @Override
    public boolean equals(Object o)
    {
        if (!super.equals(o))
            return false;

        if (!(o instanceof MovieTranscoder))
            return false;

        MovieTranscoder t = (MovieTranscoder)o;
        if (!(symbol == null ? t.symbol == null : symbol.equals(t.symbol)))
            return false;

        return true;
    }

    @Override
    public int hashCode()
    {
        int hashCode = super.hashCode();

        if (symbol != null)
            hashCode ^= symbol.hashCode();

        return hashCode;
    }
}
