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

import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.EnumSet;
import java.util.Map;

import org.apache.commons.io.IOUtils;

import org.apache.flex.compiler.common.ISourceLocation;
import org.apache.flex.compiler.constants.INamespaceConstants;
import org.apache.flex.compiler.filespecs.IBinaryFileSpecification;
import org.apache.flex.compiler.internal.embedding.EmbedAttribute;
import org.apache.flex.compiler.internal.embedding.EmbedData;
import org.apache.flex.compiler.internal.embedding.EmbedMIMEType;
import org.apache.flex.compiler.internal.semantics.PostProcessStep;
import org.apache.flex.compiler.internal.tree.as.ClassNode;
import org.apache.flex.compiler.internal.tree.as.FileNode;
import org.apache.flex.compiler.internal.tree.as.IdentifierNode;
import org.apache.flex.compiler.internal.tree.as.ImportNode;
import org.apache.flex.compiler.internal.tree.as.NamespaceIdentifierNode;
import org.apache.flex.compiler.internal.tree.as.PackageNode;
import org.apache.flex.compiler.internal.tree.as.ScopedBlockNode;
import org.apache.flex.compiler.internal.workspaces.Workspace;
import org.apache.flex.compiler.problems.EmbedUnableToReadSourceProblem;
import org.apache.flex.compiler.problems.EmbedUnsupportedAttributeProblem;
import org.apache.flex.compiler.problems.ICompilerProblem;
import org.apache.flex.compiler.projects.ICompilerProject;
import org.apache.flex.swc.ISWCFileEntry;
import org.apache.flex.swf.tags.DefineBinaryDataTag;
import org.apache.flex.swf.tags.ICharacterTag;
import org.apache.flex.swf.tags.ITag;

/**
 * Base class for all embed transcoders
 */
public abstract class TranscoderBase
{
    public static final String CORE_PACKAGE = "mx.core";
    protected static final String UTILS_PACKAGE = "mx.utils";
    protected static final String byteArrayNamePostfix = "ByteArray";

    /**
     * @param data
     * @param workspace
     */
    protected TranscoderBase(EmbedData data, Workspace workspace)
    {
        this.data = data;
        this.workspace = workspace;
        this.swcSource = data.getSWCSource();
    }

    protected final EmbedData data;
    protected final Workspace workspace;
    protected final ISWCFileEntry swcSource;
    protected String baseClassQName;
    protected String source;
    private EmbedMIMEType mimeType;

    /**
     * Transcode the embedded asset
     * @param tags The collection of transcoded asset tags
     * @param problems The collection of compiler problems to which this method will add problems.
     * @return map of symbol name to character asset tags.  null if error
     */
    protected abstract Map<String, ICharacterTag> doTranscode(Collection<ITag> tags, Collection<ICompilerProblem> problems);

    /**
     * The relevant SWF tags
     * @param tags The transcoded asset tags
     * @param problems The collection of compiler problems to which this method will add problems.
     * @return map of symbol name to character asset tags.  null if error.  the
     * returned map may not be modified.
     */
    public Map<String, ICharacterTag> getTags(Collection<ITag> tags, Collection<ICompilerProblem> problems)
    {
        return doTranscode(tags, problems);
    }

    public String getBaseClassQName()
    {
        return baseClassQName;
    }

    /**
     * @return The name of the base class of the generated class
     */
    public String getBaseClassName()
    {
        return baseClassQName.substring(baseClassQName.lastIndexOf(".") + 1);
    }

    /**
     * Analyze the attributes
     * @param location Source location from where the embed came from
     * @param problems Any problems discovered in the EmbedNode
     * @return false if analyze failed
     */
    public boolean analyze(ISourceLocation location, Collection<ICompilerProblem> problems)
    {
        boolean result = true;
        for (EmbedAttribute attribute : data.getAttributes())
        {
            if (!setAttribute(attribute))
            {
                problems.add(new EmbedUnsupportedAttributeProblem(location, attribute, (EmbedMIMEType)data.getAttribute(EmbedAttribute.MIME_TYPE)));
                result = false;
            }
        }

        if (!checkAttributeValues(location, problems))
            result = false;

        return result;
    }

    /**
     * Stores the supported attribute of the transcoder
     * @param attribute
     * @return false if a non-supported attribute
     */
    protected boolean setAttribute(EmbedAttribute attribute)
    {
        boolean isSupported = true;
        switch (attribute)
        {
            case SOURCE:
                source = (String)data.getAttribute(EmbedAttribute.SOURCE);
                break;
            case MIME_TYPE:
                mimeType = (EmbedMIMEType)data.getAttribute(EmbedAttribute.MIME_TYPE);
                break;
            default:
                isSupported = false;
        }

        return isSupported;
    }

    /**
     * Verify the attributes are valid for the specific transcoder
     * @param location
     * @param problems
     * @return false if contains invalid attributes
     */
    protected boolean checkAttributeValues(ISourceLocation location, Collection<ICompilerProblem> problems)
    {
        return true;
    }

    protected DefineBinaryDataTag buildBinaryDataTag(Collection<ICompilerProblem> problems)
    {
        byte[] bytes = getDataBytes(problems);
        if (bytes == null)
            return null;

        return new DefineBinaryDataTag(bytes);
    }

    /**
     * Get the input stream of the embedded asset
     * 
     * @param problems The collection of compiler problems to which this method will add problems.
     * @return resultant stream.  null on error
     */
    protected InputStream getDataStream(Collection<ICompilerProblem> problems)
    {
        InputStream inStrm = null;
        if (swcSource != null)
        {
            inStrm = getDataStream(swcSource, problems);
        }
        else
        {
            IBinaryFileSpecification fileSpec = workspace.getLatestBinaryFileSpecification(source);
            inStrm = getDataStream(fileSpec, problems);
        }

        return inStrm;
    }

    private InputStream getDataStream(ISWCFileEntry swcSource, Collection<ICompilerProblem> problems)
    {
        InputStream inStrm = null;
        try
        {
            inStrm = swcSource.createInputStream();
        }
        catch (IOException e)
        {
            problems.add(new EmbedUnableToReadSourceProblem(e, swcSource.getPath()));
        }

        return inStrm;
    }

    private InputStream getDataStream(IBinaryFileSpecification fileSpec, Collection<ICompilerProblem> problems)
    {
        InputStream inStrm = null;
        try
        {
            inStrm = fileSpec.createInputStream();
        }
        catch (IOException e)
        {
            problems.add(new EmbedUnableToReadSourceProblem(e, fileSpec.getPath()));
        }

        return inStrm;
    }

    /**
     * Read the original bytes of the embedded asset
     * 
     * @param problems
     * @return resultant bytes
     */
    protected byte[] getDataBytes(Collection<ICompilerProblem> problems)
    {
        InputStream inStrm = getDataStream(problems);
        if (inStrm == null)
            return null;

        byte[] bytes = null;
        try
        {
            bytes = IOUtils.toByteArray(inStrm);
        }
        catch (IOException e)
        {
            problems.add(new EmbedUnableToReadSourceProblem(e, source));
        }
        finally
        {
            if (inStrm != null)
            {
                try
                {
                    inStrm.close();
                }
                catch (IOException e)
                {
                    // don't care.  error case anyway...
                }
            }
        }

        return bytes;
    }
    
    /**
     * Build an AST to represent the embedded asset class
     * 
     * @param problems The collection of compiler problems to which this method will add problems.
     * @param filename The path to the file being embedded.
     * @return generated class AST
     */
    public FileNode buildAST(Collection<ICompilerProblem> problems, String filename)
    {
        FileNode fileNode = new FileNode(workspace, filename);
        PackageNode packageNode = new PackageNode(new IdentifierNode(""), null);
        fileNode.addItem(packageNode);

        ScopedBlockNode contents = packageNode.getScopedNode();
        ImportNode importNode = ImportNode.buildImportNode(getBaseClassQName());
        contents.addItem(importNode);

        ClassNode classNode = new ClassNode(new IdentifierNode(data.getQName()));
        classNode.setBaseClass(new IdentifierNode(getBaseClassName()));
        classNode.setNamespace(new NamespaceIdentifierNode(INamespaceConstants.public_));
        contents.addItem(classNode);

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

        return fileNode;
    }

    /**
     * Build ABC to represent the embedded asset class
     * 
     * @param project The compiler project.
     * @param problems The collecton of compiler problems to which this method will add problems.
     * @return generated class ABC
     */
    public byte[] buildABC(ICompilerProject project, Collection<ICompilerProblem> problems)
    {
        assert false : "unimplemented buildABC called";
        return null;
    }

    @Override
    public boolean equals(Object o)
    {
        if (this == o)
            return true;

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

        TranscoderBase t = (TranscoderBase)o;
        if (!source.equals(t.source) ||
            !baseClassQName.equals(t.baseClassQName) ||
            mimeType != t.mimeType)
        {
            return false;
        }

        if ((swcSource == null) && (t.swcSource != null) ||
            (swcSource != null) && (t.swcSource == null))
        {
            return false;
        }

        if (swcSource != null)
        {
            if (!swcSource.getContainingSWCPath().equals(t.swcSource.getContainingSWCPath()) ||
                !swcSource.getPath().equals(t.swcSource.getPath()))
            {
                return false;
            }
        }

        return true;
    }

    @Override
    public int hashCode()
    {
        int hashCode = 0;
        hashCode = source.hashCode();
        hashCode ^= baseClassQName.hashCode();
        hashCode ^= mimeType.toString().hashCode(); // the toString() gives us a reproducible hash code

        // if there is a swcSource, just use the two paths, and
        // ignore mod time, as we don't take that into account
        // for regular files
        if (swcSource != null)
        {
            hashCode ^= swcSource.getContainingSWCPath().hashCode();
            hashCode ^= swcSource.getPath().hashCode();
        }

        return hashCode;
    }
}
