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

import java.util.ArrayList;
import java.util.List;

import org.apache.xmpbox.XMPMetadata;

/**
 * Object returned by the validate method of the PDFValidator. This object contains a boolean to know if the PDF is
 * PDF/A-1<I>x</I> compliant. If the document isn't PDF/A-1<I>x</I> a list of errors is provided.
 */
public class ValidationResult
{
    /**
     * Boolean to know if the PDF is a valid PDF/A
     */
    private boolean isValid = false;

    /**
     * Errors to know why the PDF isn't valid. If the PDF is valid, this list is empty.
     */
    private List<ValidationError> lErrors = new ArrayList<>();

    /**
     * Object representation of the XMPMetaData contained by the pdf file This attribute can be null if the Validation
     * fails.
     */
    private XMPMetadata xmpMetaData = null;

    /**
     * Create a Validation result object
     * 
     * @param isValid
     */
    public ValidationResult(boolean isValid)
    {
        this.isValid = isValid;
    }

    /**
     * Create a Validation Result object. This constructor force the isValid to false and add the given error to the
     * list or ValidationErrors.
     * 
     * @param error
     *            if error is null, no error is added to the list.
     */
    public ValidationResult(ValidationError error)
    {
        this.isValid = false;
        if (error != null)
        {
            this.lErrors.add(error);
        }
    }

    /**
     * Create a Validation Result object. This constructor force the isValid to false and add all the given errors to
     * the list or ValidationErrors.
     * 
     * @param errors
     *            if error is null, no error is added to the list.
     */
    public ValidationResult(List<ValidationError> errors)
    {
        this.isValid = false;
        this.lErrors = errors;
    }

    /**
     * Add the ValidationError object of the otherResult in the Error list of the current object. Apply a logical AND on
     * the isValid boolean.
     * 
     * @param otherResult
     */
    public void mergeResult(ValidationResult otherResult)
    {
        if (otherResult != null)
        {
            this.lErrors.addAll(otherResult.getErrorsList());
            this.isValid &= otherResult.isValid();
        }
    }

    /**
     * @return the xmpMetaData
     */
    public XMPMetadata getXmpMetaData()
    {
        return xmpMetaData;
    }

    /**
     * @param xmpMetaData
     *            the xmpMetaData to set
     */
    void setXmpMetaData(XMPMetadata xmpMetaData)
    {
        this.xmpMetaData = xmpMetaData;
    }

    /**
     * @return true if the PDF is valid,false otherwise
     */
    public boolean isValid()
    {
        return isValid;
    }

    /**
     * Add error to the list of ValidationError. If the given error is null, this method does nothing
     * 
     * @param error
     */
    public void addError(ValidationError error)
    {
        if (error != null)
        {
            this.isValid &= error.isWarning();
            this.lErrors.add(error);
        }
    }

    /**
     * Add a set of errors to the list of ValidationError. If the given list is null, this method does nothing.
     * 
     * @param errors
     */
    public void addErrors(List<ValidationError> errors)
    {
        if (errors != null)
        {
            errors.forEach(this::addError);
        }
    }

    /**
     * @return the list of validation errors
     */
    public List<ValidationError> getErrorsList()
    {
        return this.lErrors;
    }

    /**
     * This Class represents an error of validation. It contains an error code and an error explanation.
     */
    public static class ValidationError
    {
        /**
         * Error identifier. This error code can be used as identifier to internationalize the logging message using
         * i18n.
         */
        private final String errorCode;

        /**
         * Error details
         */
        private String details;

        /**
         * false: this error can't be ignored; true: this error can be ignored
         */
        private boolean isWarning = false;

        // TODO Add here COSObject or the PDObject that is linked to the error may a automatic fix can be done.

        /**
         * Always record the place in the source code where the ValidationError
         * was created, in case the ValidationError was not caused by a
         * Throwable.
         */
        private Throwable t = null;

        /**
         * Get the place where the ValidationError was created, useful if the
         * ValidationError was not caused by a Throwable.
         *
         * @return The place where the ValidationError was created.
         */
        public Throwable getThrowable()
        {
            return t;
        }

        /**
         * The underlying cause if the ValidationError was caused by a Throwable.
         */
        private Throwable cause = null;
        
        /**
         * The page number on which the error happened, if known.
         */
        private Integer pageNumber = null;

        /**
         * Get the underlying cause if the ValidationError was caused by a
         * Throwable.
         *
         * @return The underlying cause if the ValidationError was caused by a
         * Throwable, or null if not.
         */
        public Throwable getCause()
        {
            return cause;
        }

        /**
         * Returns the page number, or null if not known.
         */
        public Integer getPageNumber()
        {
            return pageNumber;
        }

        /**
         * Sets or resets the page number.
         *
         * @param pageNumber zero based page number or null if none is known.
         */
        public void setPageNumber(Integer pageNumber)
        {
            this.pageNumber = pageNumber;
        }        

        /**
         * Create a validation error with the given error code
         * 
         * @param errorCode
         */
        public ValidationError(String errorCode)
        {
            this.errorCode = errorCode;
            if (errorCode.startsWith(PreflightConstants.ERROR_SYNTAX_COMMON))
            {
                this.details = "Syntax error";
            }
            else if (errorCode.startsWith(PreflightConstants.ERROR_SYNTAX_HEADER))
            {
                this.details = "Header Syntax error";
            }
            else if (errorCode.startsWith(PreflightConstants.ERROR_SYNTAX_BODY))
            {
                this.details = "Body Syntax error";
            }
            else if (errorCode.startsWith(PreflightConstants.ERROR_SYNTAX_CROSS_REF))
            {
                this.details = "CrossRef Syntax error";
            }
            else if (errorCode.startsWith(PreflightConstants.ERROR_SYNTAX_TRAILER_OUTLINES_INVALID))
            {
                this.details = "Outlines invalid";
            }
            else if (errorCode.startsWith(PreflightConstants.ERROR_SYNTAX_TRAILER))
            {
                this.details = "Trailer Syntax error";
            }
            else if (errorCode.startsWith(PreflightConstants.ERROR_GRAPHIC_INVALID))
            {
                this.details = "Invalid graphics object";
            }
            else if (errorCode.startsWith(PreflightConstants.ERROR_GRAPHIC_TRANSPARENCY))
            {
                this.details = "Invalid graphics transparency";
            }
            else if (errorCode.startsWith(PreflightConstants.ERROR_GRAPHIC_UNEXPECTED_VALUE_FOR_KEY))
            {
                this.details = "Unexpected value for key in Graphic object definition";
            }
            else if (errorCode.startsWith(PreflightConstants.ERROR_GRAPHIC_UNEXPECTED_KEY))
            {
                this.details = "Unexpected key in Graphic object definition";
            }
            else if (errorCode.startsWith(PreflightConstants.ERROR_GRAPHIC_INVALID_COLOR_SPACE))
            {
                this.details = "Invalid Color space";
            }
            else if (errorCode.startsWith(PreflightConstants.ERROR_GRAPHIC_MAIN))
            {
                this.details = "Unknown graphics error";
            }
            else if (errorCode.startsWith(PreflightConstants.ERROR_FONTS_INVALID_DATA))
            {
                this.details = "Invalid Font definition";
            }
            else if (errorCode.startsWith(PreflightConstants.ERROR_FONTS_DAMAGED))
            {
                this.details = "Font damaged";
            }
            else if (errorCode.startsWith(PreflightConstants.ERROR_FONTS_GLYPH))
            {
                this.details = "Glyph error";
            }
            else if (errorCode.startsWith(PreflightConstants.ERROR_TRANSPARENCY_EXT_GRAPHICAL_STATE))
            {
                this.details = "Transparency error";
            }
            else if (errorCode.startsWith(PreflightConstants.ERROR_ANNOT_MISSING_FIELDS))
            {
                this.details = "Missing field in an annotation definition";
            }
            else if (errorCode.startsWith(PreflightConstants.ERROR_ANNOT_FORBIDDEN_ELEMENT))
            {
                this.details = "Forbidden field in an annotation definition";
            }
            else if (errorCode.startsWith(PreflightConstants.ERROR_ANNOT_INVALID_ELEMENT))
            {
                this.details = "Invalid field value in an annotation definition";
            }
            else if (errorCode.startsWith(PreflightConstants.ERROR_ACTION_INVALID_ACTIONS))
            {
                this.details = "Invalid action definition";
            }
            else if (errorCode.startsWith(PreflightConstants.ERROR_ACTION_FORBIDDEN_ACTIONS))
            {
                this.details = "Action is forbidden";
            }
            else if (errorCode.startsWith(PreflightConstants.ERROR_METADATA_MAIN))
            {
                this.details = "Error on MetaData";
            }
            else if (errorCode.startsWith(PreflightConstants.ERROR_PDF_PROCESSING_MISSING))
            {
                this.details = "A Mandatory element is missing";
            }
            else
            {
                // default Unknown error
                this.details = "Unknown error";
            }
            t = new Exception();
        }

        /**
         * Create a validation error with the given error code and the error
         * explanation.
         *
         * @param errorCode the error code
         * @param details the error explanation
         * @param cause the error cause
         */
        public ValidationError(String errorCode, String details, Throwable cause)
        {
            this(errorCode);
            if (details != null)
            {
                StringBuilder sb = new StringBuilder(this.details.length() + details.length() + 2);
                sb.append(this.details).append(", ").append(details);
                this.details = sb.length() > 400 ? sb.substring(0, 400) : sb.toString();
            }
            this.cause = cause;
            t = new Exception();
        }

        /**
         * Create a validation error with the given error code and the error
         * explanation.
         *
         * @param errorCode the error code
         * @param details the error explanation
         */
        public ValidationError(String errorCode, String details)
        {
            this(errorCode, details, null);
        }        

        /**
         * @return the error code
         */
        public String getErrorCode()
        {
            return errorCode;
        }

        /**
         * @return the error explanation
         */
        public String getDetails()
        {
            return details;
        }

        /**
         * Set the error explanation
         * 
         * @param details
         */
        public void setDetails(String details)
        {
            this.details = details;
        }

        public boolean isWarning()
        {
            return isWarning;
        }

        public void setWarning(boolean isWarning)
        {
            this.isWarning = isWarning;
        }

        @Override
        public int hashCode()
        {
            return errorCode.hashCode();
        }

        @Override
        public boolean equals(Object o)
        {
            if (o instanceof ValidationError)
            {
                ValidationError ve = (ValidationError) o;
                // check errorCode
                if (!errorCode.equals(ve.errorCode))
                {
                    return false;
                }
                else if (!details.equals(ve.details))
                {
                    return false;
                }
                else if (pageNumber != null && ve.pageNumber == null)
                {
                    return false;
                }
                else if (pageNumber == null && ve.pageNumber != null)
                {
                    return false;
                }
                else if (pageNumber != null && ve.pageNumber != null
                        && pageNumber.compareTo(ve.pageNumber) != 0)
                {
                    return false;
                }
                // check warning
                return isWarning == ve.isWarning;
            }
            else
            {
                return false;
            }
        }
    }
}
