/*
 * 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.rat.report.xml;

import java.io.IOException;
import java.util.Calendar;
import java.util.Locale;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.rat.VersionInfo;
import org.apache.rat.api.Document;
import org.apache.rat.api.MetaData;
import org.apache.rat.api.RatException;
import org.apache.rat.license.ILicense;
import org.apache.rat.report.xml.writer.IXmlWriter;
import org.apache.rat.utils.CasedString;

/**
 * Creates the elements in the XML report.
 */
public class XmlElements {
    /**
     * Converts an enum name to snake case.
     * @param name the enum name to convert.
     * @return a camel cased name.
     */
    private static String normalizeName(final String name) {
        CasedString casedName = new CasedString(CasedString.StringCase.SNAKE, name.toLowerCase(Locale.ROOT));
       return casedName.toCase(CasedString.StringCase.CAMEL);
    }

    /**
     * The elements in the report.
     */
    public enum Elements {
        /** The start of the RAT report */
        RAT_REPORT("rat-report"),
        /** The version of RAT being run */
        VERSION(),
        /** A resource element */
        RESOURCE(),
        /** A license element */
        LICENSE(),
        /** A notes element */
        NOTES(),
        /** A statistics element */
        STATISTICS(),
        /** A statistic entry */
        STATISTIC(),
        /** A license name entry */
        LICENSE_NAME(),
        /** A license category entry */
        LICENSE_CATEGORY(),
        /** A document type entry */
        DOCUMENT_TYPE();

        /** The XML name for the element */
        private final String elementName;

        /**
         * Constructor.
         * @param elementName the XML name for the element.
         */
        Elements(final String elementName) {
            this.elementName = elementName;
        }

        Elements() {
            this.elementName = normalizeName(name());
        }

        /**
         * Gets the XML element name.
         * @return the XML element name.
         */
        public String getElementName() {
            return elementName;
        }
    }

    /**
     * The attributes of elements in the report.
     */
    public enum Attributes {
        /** A timestamp */
        TIMESTAMP,
        /** A version string */
        VERSION,
        /** The product identifier */
        PRODUCT,
        /** The vendor identifier */
        VENDOR,
        /** The approval flag */
        APPROVAL,
        /** The family category */
        FAMILY,
        /** The document type */
        TYPE,
        /** The id */
        ID,
        /** The name */
        NAME,
        /** A counter */
        COUNT,
        /** A description */
        DESCRIPTION,
        /** The media type for a document */
        MEDIA_TYPE,
        /** The encoding for a text document */
        ENCODING,
        /** Denotes a skipped directory */
        IS_DIRECTORY
    }

    /** The XMLWriter that we write to */
    private final IXmlWriter writer;

    /**
     * Constructor.
     * @param xmlWriter The writer to use.
     */
    public XmlElements(final IXmlWriter xmlWriter) {
        this.writer = xmlWriter;
    }

    /**
     * Create the RAT report element. Includes the timestamp and the version element.
     * @return this.
     * @throws RatException on error
     */
    public XmlElements ratReport() throws RatException {
        return write(Elements.RAT_REPORT).
                write(Attributes.TIMESTAMP, DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.format(Calendar.getInstance()))
                .version();
    }

    /**
     * Creates the version element with all version attributes populated. Closes the version element.
     * @return this.
     * @throws RatException on error
     */
    public XmlElements version() throws RatException {
        VersionInfo versionInfo = new VersionInfo();
        return write(Elements.VERSION)
                .write(Attributes.PRODUCT, versionInfo.getTitle())
                .write(Attributes.VENDOR, versionInfo.getVendor())
                .write(Attributes.VERSION, versionInfo.getVersion())
                .closeElement();
    }

    /**
     * Creates a license element. Closes the element before exit.
     * @param license the license for the element.
     * @param approved {@code true} if the license is approved.
     * @return this
     * @throws RatException on error.
     */
    public XmlElements license(final ILicense license, final boolean approved) throws RatException {
        write(Elements.LICENSE).write(Attributes.ID, license.getId())
                .write(Attributes.NAME, license.getName())
                .write(Attributes.APPROVAL, Boolean.valueOf(approved).toString())
                .write(Attributes.FAMILY, license.getLicenseFamily().getFamilyCategory());
        if (StringUtils.isNotBlank(license.getNote())) {
            try {
                write(Elements.NOTES).cdata(license.getNote()).closeElement();
            } catch (IOException e) {
                throw new RatException("Can not write CDATA for 'notes' element", e);
            }
        }
        return closeElement();
    }

    /**
     * Writes a CDATA block
     * @param data the data to write.
     * @return this
     * @throws IOException on error.
     */
    private XmlElements cdata(final String data) throws IOException {
        writer.cdata(data);
        return this;
    }

    /**
     * Creates a document element with attributes. Does NOT close the document element.
     * @param document the document to write.
     * @return this
     * @throws RatException on error.
     */
    public XmlElements document(final Document document) throws RatException {
        final MetaData metaData = document.getMetaData();
        XmlElements result = write(Elements.RESOURCE)
                .write(Attributes.NAME, document.getName().localized("/"))
                .write(Attributes.TYPE, metaData.getDocumentType().toString())
                .write(Attributes.MEDIA_TYPE, metaData.getMediaType().toString());
        if (Document.Type.STANDARD == metaData.getDocumentType() || metaData.hasCharset()) {
            result = result.write(Attributes.ENCODING, metaData.getCharset().displayName());
        }
        if (document.isIgnored()) {
            result = result.write(Attributes.IS_DIRECTORY, Boolean.toString(document.isDirectory()));
        }
        return result;
    }

    /**
     * Creates a statistics element.
     * @return this
     * @throws RatException on error.
     */
    public XmlElements statistics() throws RatException {
        return write(Elements.STATISTICS);
    }

    /**
     * Creates a statistic element. Closes the element before returning.
     * @param name the name of the statistics element.
     * @param count the count for the element.
     * @param description description of this statistic.
     * @param isOk if {@code true} the count is within limits.
     * @return this
     * @throws RatException on error.
     */
    public XmlElements statistic(final String name, final int count, final String description, final boolean isOk) throws RatException {
        return write(Elements.STATISTIC)
                .write(Attributes.NAME, name)
                .write(Attributes.COUNT, Integer.toString(count))
                .write(Attributes.APPROVAL, Boolean.toString(isOk))
                .write(Attributes.DESCRIPTION, description)
                .closeElement();
    }

    /**
     * Creates a statistic element. Closes the element before returning.
     * @param name the name of the statistics element.
     * @param count the count for the element.
     * @return this
     * @throws RatException on error.
     */
    public XmlElements licenseCategory(final String name, final int count) throws RatException {
        return write(Elements.LICENSE_CATEGORY)
                .write(Attributes.NAME, name)
                .write(Attributes.COUNT, Integer.toString(count))
                .closeElement();
    }

    /**
     * Creates a statistic element. Closes the element before returning.
     * @param name the name of the statistics element.
     * @param count the count for the element.
     * @return this
     * @throws RatException on error.
     */
    public XmlElements licenseName(final String name, final int count) throws RatException {
        return write(Elements.LICENSE_NAME)
                .write(Attributes.NAME, name)
                .write(Attributes.COUNT, Integer.toString(count))
                .closeElement();
    }

    /**
     * Creates a statistic element. Closes the element before returning.
     * @param name the name of the statistics element.
     * @param count the count for the element.
     * @return this
     * @throws RatException on error.
     */
    public XmlElements documentType(final String name, final int count) throws RatException {
        return write(Elements.DOCUMENT_TYPE)
                .write(Attributes.NAME, name)
                .write(Attributes.COUNT, Integer.toString(count))
                .closeElement();
    }

    /**
     * Closes the currently open element.
     * @return this
     * @throws RatException on error.
     */
    public XmlElements closeElement() throws RatException {
        try {
            writer.closeElement();
            return this;
        } catch (IOException e) {
            throw new RatException("Cannot close currently open element", e);
        }

    }

    /**
     * Write an element. The element is not closed.
     * @param element the element to write.
     * @return this
     * @throws RatException on error.
     */
    private XmlElements write(final Elements element) throws RatException {
        try {
            writer.openElement(element.getElementName());
            return this;
        } catch (IOException e) {
            throw new RatException("Cannot open start element: " + element.elementName, e);
        }
    }

    /**
     * Write an attribute.
     * @param attribute the attribute name.
     * @param value the attribute value.
     * @return this
     * @throws RatException on error.
     */
    public XmlElements write(final Attributes attribute, final String value) throws RatException {
        try {
            writer.attribute(normalizeName(attribute.name()), value);
            return this;
        } catch (IOException e) {
            throw new RatException("Cannot open add attribute: " + attribute, e);
        }
    }
}
