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

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.zip.ZipFile;

import org.antlr.runtime.ANTLRFileStream;
import org.antlr.runtime.ANTLRInputStream;
import org.apache.commons.io.IOUtils;

import org.apache.royale.compiler.caches.ICSSDocumentCache;
import org.apache.royale.compiler.config.Configuration;
import org.apache.royale.compiler.css.ICSSDocument;
import org.apache.royale.compiler.css.ICSSFontFace;
import org.apache.royale.compiler.css.ICSSNamespaceDefinition;
import org.apache.royale.compiler.css.ICSSNode;
import org.apache.royale.compiler.css.ICSSRule;
import org.apache.royale.compiler.internal.css.CSSDocument;
import org.apache.royale.compiler.internal.css.CSSModelTreeType;
import org.apache.royale.compiler.problems.ICompilerProblem;
import org.apache.royale.swc.ISWC;
import org.apache.royale.swc.io.SWCReader;
import org.apache.royale.utils.FilenameNormalization;
import com.google.common.collect.ImmutableList;

/**
 * Cache for {@link ICSSDocument} at workspace level. The CSS model can be a
 * "defaults.css" file inside a SWC library, or a CSS file on the disk.
 * <p>
 * The cache key is normalized path to the SWC file (optional) and the CSS file
 * name inside the SWC. The cache value is an {@link ICSSDocument}.
 */
public class CSSDocumentCache extends ConcurrentCacheStoreBase<ICSSDocument> implements ICSSDocumentCache
{

    /**
     * Since {@link ConcurrentCacheStoreBase#get} doesn't return compiler
     * problems, when there's problem parsing CSS file in
     * {@link #createEntryValue}, we have to throw a runtime exception to pass
     * the compiler problems to the caller of the cache store.
     */
    public static class ProblemParsingCSSRuntimeException extends RuntimeException
    {
        private static final long serialVersionUID = 156921800741800866L;

        public ProblemParsingCSSRuntimeException(final Collection<ICompilerProblem> problems)
        {
            super();
            this.cssParserProblems = problems;
        }

        /**
         * A collection of compiler problems from parsing the CSS file.
         */
        public final Collection<ICompilerProblem> cssParserProblems;
    }

    /**
     * Since {@link ConcurrentCacheStoreBase} does not allow null values, when a
     * SWC library does not have a "defaults.css" file, this dummy value is
     * used.
     */
    public static final ICSSDocument EMPTY_CSS_DOCUMENT = new ICSSDocument()
    {
        @Override
        public ImmutableList<ICSSRule> getRules()
        {
            return ImmutableList.of();
        }

        @Override
        public ICSSNamespaceDefinition getNamespaceDefinition(String prefix)
        {
            return null;
        }

        @Override
        public ImmutableList<ICSSFontFace> getFontFaces()
        {
            return ImmutableList.of();
        }

        @Override
        public ICSSNamespaceDefinition getDefaultNamespaceDefinition()
        {
            return null;
        }

        @Override
        public ImmutableList<ICSSNamespaceDefinition> getAtNamespaces()
        {
            return ImmutableList.of();
        }

        @Override
        public String toStringTree()
        {
            return null;
        }

        @Override
        public int getArity()
        {
            return 0;
        }

        @Override
        public ICSSNode getNthChild(int index)
        {
            throw new IllegalStateException();
        }

        @Override
        public CSSModelTreeType getOperator()
        {
            throw new IllegalStateException();
        }

        @Override
        public ICSSNode getParent()
        {
            return null;
        }

        @Override
        public String getSourcePath()
        {
            // TODO Auto-generated method stub
            return null;
        }

        @Override
        public int getStart()
        {
            return 0;
        }

        @Override
        public int getEnd()
        {
            return 0;
        }

        @Override
        public int getLine()
        {
            return 0;
        }

        @Override
        public int getColumn()
        {
            return 0;
        }

        @Override
        public int getEndLine()
        {
            return 0;
        }

        @Override
        public int getEndColumn()
        {
            return 0;
        }

        @Override
        public int getAbsoluteStart()
        {
            return 0;
        }

        @Override
        public int getAbsoluteEnd()
        {
            return 0;
        }

    };

    private abstract static class CSSDocumentCacheKeyBase extends CacheStoreKeyBase
    {
        abstract ICSSDocument parse() throws IOException;
    }
    
    /**
     * Key object for {@code CSSDocumentCache}. It the combination of a
     * normalized SWC file path and the CSS file inside the SWC. If the
     * {@code swcFile} is null, the {@code cssFileName} points to a CSS disk
     * file.
     */
    public static class CSSDocumentCacheKey extends CSSDocumentCacheKeyBase
    {
        public final ISWC swc;
        public final String cssFileName;

        public CSSDocumentCacheKey(final ISWC swc, final String cssFileName)
        {
            assert cssFileName != null : "CSS file name can't be null.";
            this.swc = swc;
            this.cssFileName = cssFileName;
        }

        @Override
        public String generateKey()
        {
            return String.format(
                        "%s:%s",
                        FilenameNormalization.normalize(swc.getSWCFile()).getAbsolutePath(),
                        cssFileName);
        }

        /**
         * Parse a CSS file in a SWC library into {@link ICSSDocument} model. If the
         * CSS file does not exist, returns {@link #EMPTY_CSS_DOCUMENT} dummy
         * object.
         * 
         * @throws IOException IO error.
         */
        @Override
        ICSSDocument parse() throws IOException
        {
            final ZipFile zipFile = new ZipFile(swc.getSWCFile(), ZipFile.OPEN_READ);
            ICSSDocument result = EMPTY_CSS_DOCUMENT;
            InputStream input = null;
            try
            {
                input = SWCReader.getInputStream(zipFile, cssFileName);
                if (input != null)
                {
                    final ANTLRInputStream in = new ANTLRInputStream(input);
                    in.name = String.format("%s:%s", swc.getSWCFile().getName(), cssFileName);
                    final List<ICompilerProblem> problems = new ArrayList<ICompilerProblem>();
                    result = CSSDocument.parse(in, problems);
                    if (!problems.isEmpty())
                        throw new ProblemParsingCSSRuntimeException(problems);
                }
            }
            finally
            {
                IOUtils.closeQuietly(input);
                zipFile.close();
            }
            return result;
        }
    }
    
    /**
     * Key object for {@code CSSDocumentCache}. It the combination of a
     * normalized SWC file path and the CSS file inside the SWC. If the
     * {@code swcFile} is null, the {@code cssFileName} points to a CSS disk
     * file.
     */
    protected static class CSSDocumentCacheKey2 extends CSSDocumentCacheKeyBase
    {
        protected final String cssFileName; // non-null

        public CSSDocumentCacheKey2(final String cssFileName)
        {
            assert cssFileName != null : "CSS file name can't be null.";
            this.cssFileName = cssFileName;
        }

        @Override
        public String generateKey()
        {
            return cssFileName;
        }

        /**
         * parse a bare CSS file on the file system.
         */
        @Override
        ICSSDocument parse() throws IOException
        {
            final List<ICompilerProblem> problems = new ArrayList<ICompilerProblem>();
            final CSSDocument css = CSSDocument.parse(new ANTLRFileStream(cssFileName), problems);
            if (!problems.isEmpty())
                throw new ProblemParsingCSSRuntimeException(problems);
            if (css != null)
                return css;
            return EMPTY_CSS_DOCUMENT;
        }
    }

    /**
     * Create a cache key for {@code CSSDocumentCache} that references
     * a CSS file in a SWC.
     * 
     * @param swc SWC file
     * @param cssFileName CSS file name
     * @return Key for {@code CSSDocumentCache}.
     */
    public static CacheStoreKeyBase createKey(final ISWC swc, final String cssFileName)
    {
        return new CSSDocumentCacheKey(swc, cssFileName);
    }
    
    /**
     * Create a cache key for {@code CSSDocumentCache} that references
     * a CSS file on disk.
     * 
     * @param cssFileName CSS file name
     * @return Key for {@code CSSDocumentCache}.
     */
    public static CacheStoreKeyBase createKey(final String cssFileName)
    {
        return new CSSDocumentCacheKey2(cssFileName);
    }

    @Override
    protected ICSSDocument createEntryValue(CacheStoreKeyBase key)
    {
        assert key instanceof CSSDocumentCacheKeyBase : "Expected 'CSSDocumentCacheKeyBase' but got " + key.getClass().getSimpleName();
        final CSSDocumentCacheKeyBase cacheKey = (CSSDocumentCacheKeyBase)key;

        ICSSDocument result = EMPTY_CSS_DOCUMENT;
        try
        {
            result = cacheKey.parse();
        }
        catch (IOException e)
        {
            // Ignore exception and return dummy value.
        }
        return result;
    }

    public static String[] ALL_DEFAULTS_CSS_FILENAMES = {"defaults.css", "defaults-3.0.0.css" };
    
    /**
     * Get the compatible-mode default CSS filename.
     * 
     * @param version Compatible version.
     * @return Defaults CSS filename.
     */
    private static String getCompatibleModeCSSFilename(final Integer version)
    {
        if (version == null)
            return "defaults.css";
        else if (version <= Configuration.MXML_VERSION_3_0)
            return "defaults-3.0.0.css";
        else
            return "defaults.css";
    }

    /**
     * Get the "default" CSS model in a SWC library. If
     * {@code compatibility-version=3} is set, this method will try to get
     * "defaults-3.0.0.css" first. If the compatibility version isn't present,
     * it will fall back to "defaults.css".
     * 
     * @param swc SWC file.
     * @param compatibilityVersion Compatibility version, or null if the
     * compiler is not under compatibility mode.
     * @return "defaults" CSS model or null if not found
     */
    public ICSSDocument getDefaultsCSS(final ISWC swc, final Integer compatibilityVersion)
    {
        final CacheStoreKeyBase key;
        final String cssFilename = getCompatibleModeCSSFilename(compatibilityVersion);
        key = createKey(swc, cssFilename);

        final ICSSDocument css = this.get(key);
        assert css != null : "ConcurrentCacheStoreBase never caches null value.";

        if (css == CSSDocumentCache.EMPTY_CSS_DOCUMENT)
        {
            if (compatibilityVersion != null)
            {
                // If compatible CSS is not present, fall back to "defaults.css".
                return getDefaultsCSS(swc, null);
            }
            else
            {
                return null;
            }
        }
        else
        {
            return css;
        }
    }
}
