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

import java.beans.IndexedPropertyDescriptor;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.util.Iterator;

import org.apache.tomcat.util.IntrospectionUtils;
import org.apache.tomcat.util.descriptor.web.ResourceBase;
import org.apache.tomcat.util.security.Escape;

/**
 * StoreAppends generate really the xml tag elements
 */
public class StoreAppender {

    /**
     * The set of classes that represent persistable properties.
     */
    private static final Class<?>[] persistables = { String.class, Integer.class, Integer.TYPE, Boolean.class,
            Boolean.TYPE, Byte.class, Byte.TYPE, Character.class, Character.TYPE, Double.class, Double.TYPE,
            Float.class, Float.TYPE, Long.class, Long.TYPE, Short.class, Short.TYPE, InetAddress.class };

    private int pos = 0;

    /**
     * Print the closing tag.
     *
     * @param aWriter The output writer
     * @param aDesc   Store description of the current element
     *
     * @throws Exception A store error occurred
     */
    public void printCloseTag(PrintWriter aWriter, StoreDescription aDesc) throws Exception {
        aWriter.print("</");
        aWriter.print(aDesc.getTag());
        aWriter.println(">");
    }

    /**
     * Print only the open tag with all attributes.
     *
     * @param aWriter The output writer
     * @param indent  Indentation level
     * @param bean    The current bean that is stored
     * @param aDesc   Store description of the current element
     *
     * @throws Exception A store error occurred
     */
    public void printOpenTag(PrintWriter aWriter, int indent, Object bean, StoreDescription aDesc) throws Exception {
        aWriter.print("<");
        aWriter.print(aDesc.getTag());
        if (aDesc.isAttributes() && bean != null) {
            printAttributes(aWriter, indent, bean, aDesc);
        }
        aWriter.println(">");
    }

    /**
     * Print tag with all attributes
     *
     * @param aWriter The output writer
     * @param indent  Indentation level
     * @param bean    The current bean that is stored
     * @param aDesc   Store description of the current element
     *
     * @throws Exception A store error occurred
     */
    public void printTag(PrintWriter aWriter, int indent, Object bean, StoreDescription aDesc) throws Exception {
        aWriter.print("<");
        aWriter.print(aDesc.getTag());
        if (aDesc.isAttributes() && bean != null) {
            printAttributes(aWriter, indent, bean, aDesc);
        }
        aWriter.println("/>");
    }

    /**
     * Print the value from tag as content.
     *
     * @param aWriter The output writer
     * @param tag     The element name
     * @param content Element content
     *
     * @throws Exception A store error occurred
     */
    public void printTagContent(PrintWriter aWriter, String tag, String content) throws Exception {
        aWriter.print("<");
        aWriter.print(tag);
        aWriter.print(">");
        aWriter.print(Escape.xml(content));
        aWriter.print("</");
        aWriter.print(tag);
        aWriter.println(">");
    }

    /**
     * Print an array of values.
     *
     * @param aWriter  The output writer
     * @param tag      The element name
     * @param indent   Indentation level
     * @param elements Array of element values
     */
    public void printTagValueArray(PrintWriter aWriter, String tag, int indent, String[] elements) {
        if (elements != null && elements.length > 0) {
            printIndent(aWriter, indent + 2);
            aWriter.print("<");
            aWriter.print(tag);
            aWriter.print(">");
            for (int i = 0; i < elements.length; i++) {
                printIndent(aWriter, indent + 4);
                aWriter.print(elements[i]);
                if (i + 1 < elements.length) {
                    aWriter.println(",");
                }
            }
            printIndent(aWriter, indent + 2);
            aWriter.print("</");
            aWriter.print(tag);
            aWriter.println(">");
        }
    }

    /**
     * Print an array of elements.
     *
     * @param aWriter  The output writer
     * @param tag      The element name
     * @param indent   Indentation level
     * @param elements Array of elements
     *
     * @throws Exception Store error occurred
     */
    public void printTagArray(PrintWriter aWriter, String tag, int indent, String[] elements) throws Exception {
        if (elements != null) {
            for (String element : elements) {
                printIndent(aWriter, indent);
                printTagContent(aWriter, tag, element);
            }
        }
    }

    /**
     * Print some spaces.
     *
     * @param aWriter The output writer
     * @param indent  The number of spaces
     */
    public void printIndent(PrintWriter aWriter, int indent) {
        for (int i = 0; i < indent; i++) {
            aWriter.print(' ');
        }
        pos = indent;
    }

    /**
     * Store the relevant attributes of the specified JavaBean, plus a <code>className</code> attribute defining the
     * fully qualified Java class name of the bean.
     *
     * @param writer PrintWriter to which we are storing
     * @param indent Indentation level
     * @param bean   Bean whose properties are to be rendered as attributes,
     * @param desc   Store description of the current element
     *
     * @exception Exception if an exception occurs while storing
     */
    public void printAttributes(PrintWriter writer, int indent, Object bean, StoreDescription desc) throws Exception {

        printAttributes(writer, indent, true, bean, desc);

    }

    /**
     * Store the relevant attributes of the specified JavaBean.
     *
     * @param writer  PrintWriter to which we are storing
     * @param indent  Indentation level
     * @param include Should we include a <code>className</code> attribute?
     * @param bean    Bean whose properties are to be rendered as attributes,
     * @param desc    RegistryDescriptor from this bean
     *
     * @exception Exception if an exception occurs while storing
     */
    public void printAttributes(PrintWriter writer, int indent, boolean include, Object bean, StoreDescription desc)
            throws Exception {

        // Render a className attribute if requested
        if (include && !desc.isStandard()) {
            writer.print(" className=\"");
            writer.print(bean.getClass().getName());
            writer.print("\"");
        }

        // Acquire the list of properties for this bean
        PropertyDescriptor[] descriptors = Introspector.getBeanInfo(bean.getClass()).getPropertyDescriptors();
        if (descriptors == null) {
            descriptors = new PropertyDescriptor[0];
        }

        // Create blank instance
        Object bean2 = defaultInstance(bean);
        for (PropertyDescriptor descriptor : descriptors) {
            Object value = checkAttribute(desc, descriptor, descriptor.getName(), bean, bean2);
            if (value != null) {
                printAttribute(writer, indent, bean, desc, descriptor.getName(), bean2, value);
            }
        }

        if (bean instanceof ResourceBase resource) {
            for (Iterator<String> iter = resource.listProperties(); iter.hasNext();) {
                String name = iter.next();
                Object value = resource.getProperty(name);
                if (!isPersistable(value.getClass())) {
                    continue;
                }
                if (desc.isTransientAttribute(name)) {
                    continue; // Skip the specified exceptions
                }
                printValue(writer, indent, name, value);

            }
        }
    }

    /**
     * Check if the attribute should be printed.
     *
     * @param desc          RegistryDescriptor from this bean
     * @param descriptor    PropertyDescriptor from this bean property
     * @param attributeName The attribute name to store
     * @param bean          The current bean
     * @param bean2         A default instance of the bean for comparison
     *
     * @return null if the value should be skipped, the value to print otherwise
     */
    protected Object checkAttribute(StoreDescription desc, PropertyDescriptor descriptor, String attributeName,
            Object bean, Object bean2) {
        if (descriptor instanceof IndexedPropertyDescriptor) {
            return null; // Indexed properties are not persisted
        }
        if (!isPersistable(descriptor.getPropertyType()) || (descriptor.getReadMethod() == null) ||
                (descriptor.getWriteMethod() == null)) {
            return null; // Must be a read-write primitive or String
        }
        if (desc.isTransientAttribute(descriptor.getName())) {
            return null; // Skip the specified exceptions
        }
        Object value = IntrospectionUtils.getProperty(bean, descriptor.getName());
        if (value == null) {
            return null; // Null values are not persisted
        }
        Object value2 = IntrospectionUtils.getProperty(bean2, descriptor.getName());
        if (value.equals(value2)) {
            // The property has its default value
            return null;
        }
        return value;
    }

    /**
     * Store the specified of the specified JavaBean.
     *
     * @param writer        PrintWriter to which we are storing
     * @param indent        Indentation level
     * @param bean          The current bean
     * @param desc          RegistryDescriptor from this bean
     * @param attributeName The attribute name to store
     * @param bean2         A default instance of the bean for comparison
     * @param value         The attribute value
     */
    protected void printAttribute(PrintWriter writer, int indent, Object bean, StoreDescription desc,
            String attributeName, Object bean2, Object value) {
        if (isPrintValue(bean, bean2, attributeName, desc)) {
            printValue(writer, indent, attributeName, value);
        }
    }

    /**
     * Determine if the attribute value needs to be stored.
     *
     * @param bean     original bean
     * @param bean2    default bean
     * @param attrName attribute name
     * @param desc     StoreDescription from bean
     *
     * @return <code>true</code> if the value should be stored
     */
    public boolean isPrintValue(Object bean, Object bean2, String attrName, StoreDescription desc) {
        return true;
    }

    /**
     * Generate default Instance for the specified bean.
     *
     * @param bean The bean
     *
     * @return an object from same class as bean parameter
     *
     * @throws ReflectiveOperationException Error creating a new instance
     */
    public Object defaultInstance(Object bean) throws ReflectiveOperationException {
        return bean.getClass().getConstructor().newInstance();
    }

    /**
     * Print an attribute value.
     *
     * @param writer PrintWriter to which we are storing
     * @param indent Indentation level
     * @param name   Attribute name
     * @param value  Attribute value
     */
    public void printValue(PrintWriter writer, int indent, String name, Object value) {
        // Convert IP addresses to strings so they will be persisted
        if (value instanceof InetAddress) {
            value = ((InetAddress) value).getHostAddress();
        }
        if (!(value instanceof String)) {
            value = value.toString();
        }
        String strValue = Escape.xml((String) value);
        pos = pos + name.length() + strValue.length();
        if (pos > 60) {
            writer.println();
            printIndent(writer, indent + 4);
        } else {
            writer.print(' ');
        }
        writer.print(name);
        writer.print("=\"");
        writer.print(strValue);
        writer.print("\"");
    }


    /**
     * Is the specified property type one for which we should generate a persistence attribute?
     *
     * @param clazz Java class to be tested
     *
     * @return <code>true</code> if the specified class should be stored
     */
    protected boolean isPersistable(Class<?> clazz) {

        for (Class<?> persistable : persistables) {
            if (persistable == clazz || persistable.isAssignableFrom(clazz)) {
                return true;
            }
        }
        return false;

    }
}
