/*****************************************************************************
 * 
 * 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.pdfbox.preflight.process;

import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_SYNTAX_STREAM_DAMAGED;
import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_SYNTAX_STREAM_DELIMITER;
import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_SYNTAX_STREAM_FX_KEYS;
import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_SYNTAX_STREAM_INVALID_FILTER;
import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_SYNTAX_STREAM_LENGTH_INVALID;
import static org.apache.pdfbox.preflight.PreflightConstants.ERROR_SYNTAX_STREAM_LENGTH_MISSING;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSDocument;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSObject;
import org.apache.pdfbox.cos.COSStream;
import org.apache.pdfbox.io.IOUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.cos.COSObjectKey;
import org.apache.pdfbox.preflight.PreflightContext;
import org.apache.pdfbox.preflight.ValidationResult.ValidationError;
import org.apache.pdfbox.preflight.exception.ValidationException;
import org.apache.pdfbox.preflight.utils.COSUtils;
import org.apache.pdfbox.preflight.utils.FilterHelper;
import org.apache.pdfbox.util.Charsets;

public class StreamValidationProcess extends AbstractProcess
{

    private static final String ENDSTREAM = "endstream";

    @Override
    public void validate(PreflightContext ctx) throws ValidationException
    {
        PDDocument pdfDoc = ctx.getDocument();
        COSDocument cDoc = pdfDoc.getDocument();

        List<?> lCOSObj = cDoc.getObjects();
        for (Object o : lCOSObj)
        {
            COSObject cObj = (COSObject) o;
            
            // If this object represents a Stream, the Dictionary must contain the Length key
            COSBase cBase = cObj.getObject();
            if (cBase instanceof COSStream)
            {
                validateStreamObject(ctx, cObj);
            }
        }
    }

    public void validateStreamObject(PreflightContext context, COSObject cObj) throws ValidationException
    {
        COSStream streamObj = (COSStream) cObj.getObject();

        // ---- Check dictionary entries
        // ---- Only the Length entry is mandatory
        // ---- In a PDF/A file, F, FFilter and FDecodeParms are forbidden
        checkDictionaryEntries(context, streamObj);
        // ---- check stream length
        checkStreamLength(context, cObj);
        // ---- Check the Filter value(s)
        checkFilters(streamObj, context);
    }

    /**
     * This method checks if one of declared Filter is LZWdecode. If LZW is found, the result list is updated with an
     * error code.
     * 
     * @param stream the stream to check.
     * @param context the preflight context.
     */
    protected void checkFilters(COSStream stream, PreflightContext context)
    {
        COSBase bFilter = stream.getDictionaryObject(COSName.FILTER);
        if (bFilter != null)
        {
            COSDocument cosDocument = context.getDocument().getDocument();
            if (COSUtils.isArray(bFilter, cosDocument))
            {
                COSArray afName = (COSArray) bFilter;
                for (int i = 0; i < afName.size(); ++i)
                {
                    FilterHelper.isAuthorizedFilter(context, afName.getString(i));
                }
            }
            else if (bFilter instanceof COSName)
            {
                String fName = ((COSName) bFilter).getName();
                FilterHelper.isAuthorizedFilter(context, fName);
            }
            else
            {
                // ---- The filter type is invalid
                addValidationError(context, new ValidationError(ERROR_SYNTAX_STREAM_INVALID_FILTER,
                        "Filter should be a Name or an Array"));
            }
        }
        // else Filter entry is optional
    }

    private boolean readUntilEndOfDictionaryStream(InputStream ra) throws IOException
    {
        boolean search = true;
        boolean maybe = false;
        do
        {
            int c = ra.read();
            switch (c)
            {
            case '>':
                if (maybe)
                {
                    return true;
                }
                maybe = true;
                break;
            case -1:
                search = false;
                break;
            default:
                maybe = false;
                break;
            }
        } while (search);
        return false;
    }

    private boolean readUntilStream(InputStream ra) throws IOException
    {
        boolean search = true;
        boolean maybe = false;
        int lastChar = -1;
        do
        {
            int c = ra.read();
            switch (c)
            {
            case 's':
                maybe = true;
                lastChar = c;
                break;
            case 't':
                if (maybe && lastChar == 's')
                {
                    lastChar = c;
                }
                else
                {
                    maybe = false;
                    lastChar = -1;
                }
                break;
            case 'r':
                if (maybe && lastChar == 't')
                {
                    lastChar = c;
                }
                else
                {
                    maybe = false;
                    lastChar = -1;
                }
                break;
            case 'e':
                if (maybe && lastChar == 'r')
                {
                    lastChar = c;
                }
                else
                {
                    maybe = false;
                }
                break;
            case 'a':
                if (maybe && lastChar == 'e')
                {
                    lastChar = c;
                }
                else
                {
                    maybe = false;
                }
                break;
            case 'm':
                if (maybe && lastChar == 'a')
                {
                    return true;
                }
                else
                {
                    maybe = false;
                }
                break;
            case -1:
                search = false;
                break;
            default:
                maybe = false;
                break;
            }
        } while (search);
        return false;
    }

    protected void checkStreamLength(PreflightContext context, COSObject cObj) throws ValidationException
    {
        COSStream streamObj = (COSStream) cObj.getObject();
        int length = streamObj.getInt(COSName.LENGTH);
        InputStream ra = null;
        try
        {
            ra = context.getSource().getInputStream();
            Long offset = context.getDocument().getDocument().getXrefTable().get(new COSObjectKey(cObj));

            // ---- go to the beginning of the object
            long skipped = 0;
            if (offset != null)
            {
                while (skipped != offset)
                {
                    long curSkip = ra.skip(offset - skipped);
                    if (curSkip < 0)
                    {
                        addValidationError(context, new ValidationError(ERROR_SYNTAX_STREAM_DAMAGED, "Unable to skip bytes in the PDFFile to check stream length"));
                        return;
                    }
                    skipped += curSkip;
                }

                // skip (most of) the dictionary to avoid false positives, see PDFBOX-4925
                if (!readUntilEndOfDictionaryStream(ra))
                {
                    addValidationError(context, new ValidationError(ERROR_SYNTAX_STREAM_DAMAGED,
                            "Unable to find end of dictionary"));
                    return;
                }

                // ---- go to the stream key word
                if (readUntilStream(ra))
                {
                    int c = ra.read();
                    // "stream" has to be followed by a LF or CRLF
                    if ((c != '\r' && c != '\n') //
                            || (c == '\r' && ra.read() != '\n'))
                    {
                        addValidationError(context,
                                new ValidationError(ERROR_SYNTAX_STREAM_DELIMITER,
                                        "Expected 'EOL' after the stream keyword not found"));
                        return;
                    }
                    // ---- Here is the true beginning of the Stream Content.
                    // ---- Read the given length of bytes and check the 10 next bytes
                    // ---- to see if there are endstream.
                    byte[] buffer = new byte[1024];
                    int nbBytesToRead = length;

                    do
                    {
                        int cr;
                        if (nbBytesToRead > buffer.length)
                        {
                            cr = ra.read(buffer);
                        }
                        else
                        {
                            cr = ra.read(buffer, 0, nbBytesToRead);
                        }
                        if (cr == -1)
                        {
                            addStreamLengthValidationError(context, cObj, length, "");
                            return;
                        }
                        else
                        {
                            nbBytesToRead -= cr;
                        }
                    }
                    while (nbBytesToRead > 0);

                    int len = ENDSTREAM.length() + 2;
                    byte[] buffer2 = new byte[len];
                    ra.read(buffer2);

                    // ---- check the content of 10 last characters
                    // there has to be an proceeding EOL (LF or CRLF)
                    String endStream = new String(buffer2, Charsets.ISO_8859_1);
                    if ((buffer2[0] != '\r' && buffer2[0] != '\n') //
                            || (buffer2[0] == '\r' && buffer2[1] != '\n') //
                            || (buffer2[0] == '\n' && buffer2[1] != 'e') //
                            || !endStream.contains(ENDSTREAM))
                    {
                        // TODO in some cases it is hard to say if the reason for this issue is a missing EOL or a wrong
                        // stream length, see isartor-6-1-7-t03-fail-a.pdf
                        // the implementation has to be adjusted similar to PreflightParser#parseCOSStream
                        addStreamLengthValidationError(context, cObj, length, endStream);
                    }
                }
                else
                {
                    addStreamLengthValidationError(context, cObj, length, "");
                }
            }
        }
        catch (IOException e)
        {
            throw new ValidationException("Unable to read a stream to validate: " + e.getMessage(), e);
        }
        finally
        {
            IOUtils.closeQuietly(ra);
        }
    }

    /**
     * Check dictionary entries. Only the Length entry is mandatory. In a PDF/A file, F, FFilter and FDecodeParms are
     * forbidden
     * 
     * @param context the preflight context.
     * @param streamObj the stream to check.
     */
    protected void checkDictionaryEntries(PreflightContext context, COSStream streamObj)
    {
        boolean len = streamObj.containsKey(COSName.LENGTH);
        boolean f = streamObj.containsKey(COSName.F);
        boolean ffilter = streamObj.containsKey(COSName.F_FILTER);
        boolean fdecParams = streamObj.containsKey(COSName.F_DECODE_PARMS);

        if (!len)
        {
            addValidationError(context, new ValidationError(ERROR_SYNTAX_STREAM_LENGTH_MISSING,
                    "Stream length is missing"));
        }

        if (f || ffilter || fdecParams)
        {
            addValidationError(context, new ValidationError(ERROR_SYNTAX_STREAM_FX_KEYS,
                    "F, FFilter or FDecodeParms keys are present in the stream dictionary"));
        }
    }
    
    private void addStreamLengthValidationError(PreflightContext context, COSObject cObj, int length, String endStream)
    {
        addValidationError(context, new ValidationError(ERROR_SYNTAX_STREAM_LENGTH_INVALID,
                "Stream length is invalid [cObj=" + cObj + "; defined length=" + length + "; buffer2=" + endStream + "]"));
    }

}
