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

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;

import org.apache.commons.io.IOUtils;

import org.apache.royale.utils.FilenameNormalization;

/**
 * This is an {@link IFileSpecification} that include multiple source files into
 * one. JFlex-generated tokenizers can skip BOM at the beginning of a file, but
 * not elsewhere. This class skips BOM header in each source file so that the
 * merged file won't have a BOM in the middle of a file.
 */
public class CombinedFile implements IFileSpecification
{

    /**
     * Create a combined file by concatenating {@code includedFilenames}
     * together and append {@code sourceFilename} at the end.
     * 
     * @param includedFilenames included files
     * @param sourceFilename source file
     */
    public CombinedFile(final List<String> includedFilenames, final String sourceFilename)
    {
        this.combinedSource = null;
        this.sourceFilename = FilenameNormalization.normalize(sourceFilename);
        this.fileList = includedFilenames;
        this.fileList.add(sourceFilename);
    }

    private StringBuilder combinedSource;
    private final String sourceFilename;
    private final List<String> fileList;

    /**
     * The path of the combined file is the path of the source file.
     */
    @Override
    public String getPath()
    {
        return sourceFilename;
    }

    /**
     * Create a {@link Reader} from the combined source text.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public Reader createReader() throws FileNotFoundException
    {
        if (combinedSource == null)
            combineFile();
        return new StringReader(combinedSource.toString());
    }

    /**
     * Get the time stamp of the most recent modified file among the source
     * files.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public long getLastModified()
    {
        long lastModified = 0;

        for (final String file : fileList)
        {
            final long timestamp = new File(file).lastModified();
            lastModified = Math.max(lastModified, timestamp);
        }

        return lastModified;
    }

    /**
     * Always return false because the combined file is cached in memory.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public boolean isOpenDocument()
    {
        return false;
    }

    /**
     * Concatenate source files together. The main source file is always at the
     * end.
     * 
     * @throws FileNotFoundException error
     */
    private void combineFile() throws FileNotFoundException
    {
        assert combinedSource == null : "Do not call combineFile() twice.";

        combinedSource = new StringBuilder();

        for (final String filename : fileList)
        {
            Reader reader = null;
            try
            {
                final BufferedInputStream strm = getStreamAndSkipBOM(filename);
                reader = new InputStreamReader(strm);
                combinedSource.append(IOUtils.toString(reader));
                combinedSource.append(IOUtils.LINE_SEPARATOR);
            }
            catch (FileNotFoundException e)
            {
                throw e;
            }
            catch (IOException e)
            {
                throw new RuntimeException(e);
            }
            finally
            {
                IOUtils.closeQuietly(reader);
            }
        }
    }

    /**
     * BOM patterns.
     * 
     * @see <a href="http://www.unicode.org/faq/utf_bom.html#BOM">Unicode BOM spec</a>
     */
    public static enum BOM
    {
        NONE("UTF-8"),
        UTF_8("UTF-8", (byte)0xEF, (byte)0xBB, (byte)0xBF),
        UTF_16_LE("UTF-16LE", (byte)0xFF, (byte)0xFE),
        UTF_16_BE("UTF-16BE", (byte)0xFE, (byte)0xFF),
        UTF_32_LE("UTF-32LE", (byte)0xFF, (byte)0xFE, (byte)0x00, (byte)0x00),
        UTF_32_BE("UTF-32BE", (byte)0x00, (byte)0x00, (byte)0xFE, (byte)0xFF);

        private BOM(final String charsetName, final byte... pattern)
        {
            if (charsetName == null || !Charset.isSupported(charsetName))
                this.charset = Charset.defaultCharset();
            else
                this.charset = Charset.forName(charsetName);
            this.pattern = pattern;
        }

        /**
         * BOM pattern in byte array.
         */
        public final byte pattern[];

        /**
         * The Java {@link Charset} for this BOM header.
         */
        public final Charset charset;
    }

    /**
     * Get the {@link BufferedInputStream} of a file, skipping the BOM.
     * 
     * @param filename The path to the file.
     * @return BufferedInputStream
     */
    public static BufferedInputStream getStreamAndSkipBOM(String filename) throws IOException
    {
        final File file = new File(filename);
        BufferedInputStream strm = new BufferedInputStream(new FileInputStream(file));
        final BOM bom = getBOM(strm);
        strm.skip(bom.pattern.length);

        return strm;
    }

    /**
     * Get the BOM tag of a stream.
     * 
     * @param strm BufferedInputStream to be checked.
     * @return {@link BOM} type.
     * @throws IOException Error.
     */
    public static BOM getBOM(BufferedInputStream strm) throws IOException
    {
        assert (strm.markSupported()) : "getBOM call on stream which does not support mark";

        // Peek the first 4 bytes.
        final byte[] peek = new byte[4];
        strm.mark(4);
        strm.read(peek);
        strm.reset();

        // Try matching 4-byte BOM tags.
        final byte[] quadruplet = Arrays.copyOf(peek, 4);
        if (Arrays.equals(BOM.UTF_32_BE.pattern, quadruplet))
            return BOM.UTF_32_BE;
        else if (Arrays.equals(BOM.UTF_32_LE.pattern, quadruplet))
            return BOM.UTF_32_LE;

        // Try matching 3-byte BOM tags.
        final byte[] triplet = Arrays.copyOf(peek, 3);
        if (Arrays.equals(BOM.UTF_8.pattern, triplet))
            return BOM.UTF_8;

        // Try matching 2-byte BOM tags.
        final byte[] twin = Arrays.copyOf(peek, 2);
        if (Arrays.equals(BOM.UTF_16_BE.pattern, twin))
            return BOM.UTF_16_BE;
        else if (Arrays.equals(BOM.UTF_16_LE.pattern, twin))
            return BOM.UTF_16_LE;

        // No BOM tag.
        return BOM.NONE;
    }
}
