/**
 *
 * Copyright 2003-2007 Jive Software 2020-2022 Florian Schmaus.
 *
 * Licensed 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.jivesoftware.smackx.xdata.provider;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.logging.Logger;

import javax.xml.namespace.QName;

import org.jivesoftware.smack.packet.XmlEnvironment;
import org.jivesoftware.smack.parsing.SmackParsingException;
import org.jivesoftware.smack.provider.ExtensionElementProvider;
import org.jivesoftware.smack.roster.packet.RosterPacket;
import org.jivesoftware.smack.roster.provider.RosterPacketProvider;
import org.jivesoftware.smack.util.EqualsUtil;
import org.jivesoftware.smack.util.HashCode;
import org.jivesoftware.smack.xml.XmlPullParser;
import org.jivesoftware.smack.xml.XmlPullParserException;

import org.jivesoftware.smackx.formtypes.FormFieldRegistry;
import org.jivesoftware.smackx.xdata.AbstractMultiFormField;
import org.jivesoftware.smackx.xdata.AbstractSingleStringValueFormField;
import org.jivesoftware.smackx.xdata.BooleanFormField;
import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.FormFieldChildElement;
import org.jivesoftware.smackx.xdata.FormFieldWithOptions;
import org.jivesoftware.smackx.xdata.JidMultiFormField;
import org.jivesoftware.smackx.xdata.JidSingleFormField;
import org.jivesoftware.smackx.xdata.ListMultiFormField;
import org.jivesoftware.smackx.xdata.ListSingleFormField;
import org.jivesoftware.smackx.xdata.TextSingleFormField;
import org.jivesoftware.smackx.xdata.packet.DataForm;
import org.jivesoftware.smackx.xdatalayout.packet.DataLayout;
import org.jivesoftware.smackx.xdatalayout.provider.DataLayoutProvider;

/**
 * The DataFormProvider parses DataForm packets.
 *
 * @author Gaston Dombiak
 */
public class DataFormProvider extends ExtensionElementProvider<DataForm> {

    private static final Logger LOGGER = Logger.getLogger(DataFormProvider.class.getName());

    public static final DataFormProvider INSTANCE = new DataFormProvider();

    @Override
    public DataForm parse(XmlPullParser parser, int initialDepth, XmlEnvironment xmlEnvironment) throws XmlPullParserException, IOException, SmackParsingException {
        DataForm.Type dataFormType = DataForm.Type.fromString(parser.getAttributeValue("", "type"));
        DataForm.Builder dataForm = DataForm.builder(dataFormType);

        String formType = null;
        DataForm.ReportedData reportedData = null;

        outerloop: while (true) {
            XmlPullParser.Event eventType = parser.next();
            switch (eventType) {
            case START_ELEMENT:
                String name = parser.getName();
                String namespace = parser.getNamespace();
                XmlEnvironment elementXmlEnvironment = XmlEnvironment.from(parser, xmlEnvironment);
                switch (name) {
                case "instructions":
                    dataForm.addInstruction(parser.nextText());
                    break;
                case "title":
                    dataForm.setTitle(parser.nextText());
                    break;
                case "field":
                    // Note that we parse this form field without any potential reportedData. We only use reportedData
                    // to lookup form field types of fields under <item/>.
                    FormField formField = parseField(parser, elementXmlEnvironment, formType);

                    TextSingleFormField hiddenFormTypeField = formField.asHiddenFormTypeFieldIfPossible();
                    if (hiddenFormTypeField != null) {
                        if (formType != null) {
                            throw new SmackParsingException("Multiple hidden form type fields");
                        }
                        formType = hiddenFormTypeField.getValue();
                    }

                    dataForm.addField(formField);
                    break;
                case "item":
                    DataForm.Item item = parseItem(parser, elementXmlEnvironment, formType, reportedData);
                    dataForm.addItem(item);
                    break;
                case "reported":
                    if (reportedData != null) {
                        throw new SmackParsingException("Data form with multiple <reported/> elements");
                    }
                    reportedData = parseReported(parser, elementXmlEnvironment, formType);
                    dataForm.setReportedData(reportedData);
                    break;
                // See XEP-133 Example 32 for a corner case where the data form contains this extension.
                case RosterPacket.ELEMENT:
                    if (namespace.equals(RosterPacket.NAMESPACE)) {
                        dataForm.addExtensionElement(RosterPacketProvider.INSTANCE.parse(parser));
                    }
                    break;
                // See XEP-141 Data Forms Layout
                case DataLayout.ELEMENT:
                    if (namespace.equals(DataLayout.NAMESPACE)) {
                        dataForm.addExtensionElement(DataLayoutProvider.parse(parser));
                    }
                    break;
                }
                break;
            case END_ELEMENT:
                if (parser.getDepth() == initialDepth) {
                    break outerloop;
                }
                break;
            default:
                // Catch all for incomplete switch (MissingCasesInEnumSwitch) statement.
                break;
            }
        }
        return dataForm.build();
    }

    private static FormField parseField(XmlPullParser parser, XmlEnvironment xmlEnvironment, String formType)
                    throws XmlPullParserException, IOException, SmackParsingException {
        return parseField(parser, xmlEnvironment, formType, null);
    }

    private static final class FieldNameAndFormType {
        private final String fieldName;
        private final String formType;

        private FieldNameAndFormType(String fieldName, String formType) {
            this.fieldName = fieldName;
            this.formType = formType;
        }

        private final HashCode.Cache hashCodeCache = new HashCode.Cache();

        @Override
        public int hashCode() {
            return hashCodeCache.getHashCode(b ->
                           b.append(fieldName)
                            .append(formType)
                            .build()
            );
        }

        @Override
        public boolean equals(Object other) {
            return EqualsUtil.equals(this, other, (e, o) ->
                e.append(fieldName, o.fieldName)
                 .append(formType, o.formType)
            );
        }
    }

    private static final Set<FieldNameAndFormType> UNKNOWN_FIELDS = new CopyOnWriteArraySet<>();

    private static FormField parseField(XmlPullParser parser, XmlEnvironment xmlEnvironment, String formType, DataForm.ReportedData reportedData)
                    throws XmlPullParserException, IOException, SmackParsingException {
        final int initialDepth = parser.getDepth();

        final String fieldName = parser.getAttributeValue("var");
        final String label = parser.getAttributeValue("", "label");

        FormField.Type type = null;
        {
            String fieldTypeString = parser.getAttributeValue("type");
            if (fieldTypeString != null) {
                type = FormField.Type.fromString(fieldTypeString);
            }
        }

        List<FormField.Value> values = new ArrayList<>();
        List<FormField.Option> options = new ArrayList<>();
        List<FormFieldChildElement> childElements = new ArrayList<>();
        boolean required = false;

        outerloop: while (true) {
            XmlPullParser.TagEvent eventType = parser.nextTag();
            switch (eventType) {
            case START_ELEMENT:
                QName qname = parser.getQName();
                if (qname.equals(FormField.Value.QNAME)) {
                    FormField.Value value = parseValue(parser);
                    values.add(value);
                } else if (qname.equals(FormField.Option.QNAME)) {
                    FormField.Option option = parseOption(parser);
                    options.add(option);
                } else if (qname.equals(FormField.Required.QNAME)) {
                    required = true;
                } else {
                    FormFieldChildElementProvider<?> provider = FormFieldChildElementProviderManager.getFormFieldChildElementProvider(
                                    qname);
                    if (provider == null) {
                        LOGGER.warning("Unknown form field child element " + qname + " ignored");
                        continue;
                    }
                    FormFieldChildElement formFieldChildElement = provider.parse(parser,
                                    XmlEnvironment.from(parser, xmlEnvironment));
                    childElements.add(formFieldChildElement);
                }
                break;
            case END_ELEMENT:
                if (parser.getDepth() == initialDepth) {
                    break outerloop;
                }
                break;
            }
        }

        // Data forms of type 'result' may contain <reported/> and <item/> elements. If this is the case, then the type
        // of the <field/>s within the <item/> elements is determined by the information found in <reported/>. See
        // XEP-0004 § 3.4 and SMACK-902
        if (type == null && reportedData != null) {
            FormField reportedFormField = reportedData.getField(fieldName);
            if (reportedFormField != null) {
                type = reportedFormField.getType();
            }
        }

        if (type == null) {
            // The field name 'FORM_TYPE' is magic.
            if (fieldName.equals(FormField.FORM_TYPE)) {
                type = FormField.Type.hidden;
            } else {
                // If no field type was explicitly provided, then we need to lookup the
                // field's type in the registry.
                type = FormFieldRegistry.lookup(formType, fieldName);
                if (type == null) {
                    FieldNameAndFormType fieldNameAndFormType = new FieldNameAndFormType(fieldName, formType);
                    if (!UNKNOWN_FIELDS.contains(fieldNameAndFormType)) {
                        LOGGER.warning("The Field '" + fieldName + "' from FORM_TYPE '" + formType
                                        + "' is not registered. Field type is unknown, assuming text-single.");
                        UNKNOWN_FIELDS.add(fieldNameAndFormType);
                    }
                    // As per XEP-0004, text-single is the default form field type, which we use as emergency fallback here.
                    type = FormField.Type.text_single;
                }
            }
        }

        FormField.Builder<?, ?> builder;
        switch (type) {
        case bool:
            builder = parseBooleanFormField(fieldName, values);
            break;
        case fixed:
            builder = parseSingleKindFormField(FormField.fixedBuilder(fieldName), values);
            break;
        case hidden:
            builder = parseSingleKindFormField(FormField.hiddenBuilder(fieldName), values);
            break;
        case jid_multi:
            JidMultiFormField.Builder jidMultiBuilder = FormField.jidMultiBuilder(fieldName);
            for (FormField.Value value : values) {
                jidMultiBuilder.addValue(value);
            }
            builder = jidMultiBuilder;
            break;
        case jid_single:
            ensureAtMostSingleValue(type, values);
            JidSingleFormField.Builder jidSingleBuilder = FormField.jidSingleBuilder(fieldName);
            if (!values.isEmpty()) {
                FormField.Value value = values.get(0);
                jidSingleBuilder.setValue(value);
            }
            builder = jidSingleBuilder;
            break;
        case list_multi:
            ListMultiFormField.Builder listMultiBuilder = FormField.listMultiBuilder(fieldName);
            addOptionsToBuilder(options, listMultiBuilder);
            builder = parseMultiKindFormField(listMultiBuilder, values);
            break;
        case list_single:
            ListSingleFormField.Builder listSingleBuilder = FormField.listSingleBuilder(fieldName);
            addOptionsToBuilder(options, listSingleBuilder);
            builder = parseSingleKindFormField(listSingleBuilder, values);
            break;
        case text_multi:
            builder = parseMultiKindFormField(FormField.textMultiBuilder(fieldName), values);
            break;
        case text_private:
            builder = parseSingleKindFormField(FormField.textPrivateBuilder(fieldName), values);
            break;
        case text_single:
            builder = parseSingleKindFormField(FormField.textSingleBuilder(fieldName), values);
            break;
        default:
            // Should never happen, as we cover all types in the switch/case.
            throw new AssertionError("Unknown type " + type);
        }


        switch (type) {
        case list_multi:
        case list_single:
            break;
        default:
            if (!options.isEmpty()) {
                throw new SmackParsingException("Form fields of type " + type + " must not have options. This one had "
                                + options.size());
            }
            break;
        }

        if (label != null) {
            builder.setLabel(label);
        }
        builder.setRequired(required);
        builder.addFormFieldChildElements(childElements);

        return builder.build();
    }

    private static FormField.Builder<?, ?> parseBooleanFormField(String fieldName, List<FormField.Value> values) throws SmackParsingException {
        BooleanFormField.Builder builder = FormField.booleanBuilder(fieldName);
        ensureAtMostSingleValue(builder.getType(), values);
        if (values.size() == 1) {
            FormField.Value value = values.get(0);
            builder.setValue(value);
        }
        return builder;
    }

    private static AbstractSingleStringValueFormField.Builder<?, ?> parseSingleKindFormField(
                    AbstractSingleStringValueFormField.Builder<?, ?> builder, List<FormField.Value> values)
                    throws SmackParsingException {
        ensureAtMostSingleValue(builder.getType(), values);
        if (values.size() == 1) {
            String value = values.get(0).getValue().toString();
            builder.setValue(value);
        }
        return builder;
    }

    private static AbstractMultiFormField.Builder<?, ?> parseMultiKindFormField(AbstractMultiFormField.Builder<?, ?> builder,
                    List<FormField.Value> values) {
        for (FormField.Value value : values) {
            String rawValue = value.getValue().toString();
            builder.addValue(rawValue);
        }
        return builder;
    }

    private static DataForm.Item parseItem(XmlPullParser parser, XmlEnvironment xmlEnvironment, String formType,
                    DataForm.ReportedData reportedData)
                    throws XmlPullParserException, IOException, SmackParsingException {
        final int initialDepth = parser.getDepth();
        List<FormField> fields = new ArrayList<>();
        outerloop: while (true) {
            XmlPullParser.TagEvent eventType = parser.nextTag();
            switch (eventType) {
            case START_ELEMENT:
                String name = parser.getName();
                switch (name) {
                case "field":
                    FormField field = parseField(parser, XmlEnvironment.from(parser, xmlEnvironment), formType,
                                    reportedData);
                    fields.add(field);
                    break;
                }
                break;
            case END_ELEMENT:
                if (parser.getDepth() == initialDepth) {
                    break outerloop;
                }
                break;
            }
        }
        return new DataForm.Item(fields);
    }

    private static DataForm.ReportedData parseReported(XmlPullParser parser, XmlEnvironment xmlEnvironment, String formType)
                    throws XmlPullParserException, IOException, SmackParsingException {
        final int initialDepth = parser.getDepth();
        List<FormField> fields = new ArrayList<>();
        outerloop: while (true) {
            XmlPullParser.TagEvent eventType = parser.nextTag();
            switch (eventType) {
            case START_ELEMENT:
                String name = parser.getName();
                switch (name) {
                case "field":
                    FormField field = parseField(parser, XmlEnvironment.from(parser, xmlEnvironment), formType);
                    fields.add(field);
                    break;
                }
                break;
            case END_ELEMENT:
                if (parser.getDepth() == initialDepth) {
                    break outerloop;
                }
                break;
            }
        }
        return new DataForm.ReportedData(fields);
    }

    public static FormField.Value parseValue(XmlPullParser parser) throws IOException, XmlPullParserException {
        String value = parser.nextText();
        return new FormField.Value(value);
    }

    public static FormField.Option parseOption(XmlPullParser parser) throws IOException, XmlPullParserException {
        int initialDepth = parser.getDepth();
        FormField.Option option = null;
        String label = parser.getAttributeValue("", "label");
        outerloop: while (true) {
            XmlPullParser.TagEvent eventType = parser.nextTag();
            switch (eventType) {
            case START_ELEMENT:
                String name = parser.getName();
                switch (name) {
                case "value":
                    option = new FormField.Option(label, parser.nextText());
                    break;
                }
                break;
            case END_ELEMENT:
                if (parser.getDepth() == initialDepth) {
                    break outerloop;
                }
                break;
            }
        }
        return option;
    }

    private static void ensureAtMostSingleValue(FormField.Type type, List<FormField.Value> values) throws SmackParsingException {
        if (values.size() > 1) {
            throw new SmackParsingException(type + " fields can have at most one value, this one had " + values.size());
        }
    }

    private static void addOptionsToBuilder(Collection<FormField.Option> options, FormFieldWithOptions.Builder<?> builder) {
        for (FormField.Option option : options) {
            builder.addOption(option);
        }
    }
}
