package com.box.sdk;

import com.eclipsesource.json.Json;
import com.eclipsesource.json.JsonArray;
import com.eclipsesource.json.JsonObject;
import com.eclipsesource.json.JsonValue;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

/**
 * The MetadataTemplate class represents the Box metadata template object.
 * Templates allow the metadata service to provide a multitude of services,
 * such as pre-defining sets of key:value pairs or schema enforcement on specific fields.
 *
 * @see <a href="https://developer.box.com/reference/resources/metadata-templates/">Box metadata templates</a>
 */
public class MetadataTemplate extends BoxJSONObject {

    /**
     * @see #getMetadataTemplate(BoxAPIConnection)
     */
    public static final URLTemplate METADATA_TEMPLATE_URL_TEMPLATE
        = new URLTemplate("metadata_templates/%s/%s/schema");

    /**
     * @see #getMetadataTemplateByID(BoxAPIConnection, String)
     */
    public static final URLTemplate METADATA_TEMPLATE_BY_ID_URL_TEMPLATE = new URLTemplate("metadata_templates/%s");

    /**
     * @see #createMetadataTemplate(BoxAPIConnection, String, String, String, boolean, List)
     */
    public static final URLTemplate METADATA_TEMPLATE_SCHEMA_URL_TEMPLATE
        = new URLTemplate("metadata_templates/schema");

    /**
     * @see #getEnterpriseMetadataTemplates(String, int, BoxAPIConnection, String...)
     */
    public static final URLTemplate ENTERPRISE_METADATA_URL_TEMPLATE = new URLTemplate("metadata_templates/%s");

    /**
     *
     */
    private static final URLTemplate METADATA_QUERIES_URL_TEMPLATE = new URLTemplate("metadata_queries/execute_read");

    /**
     * Default metadata type to be used in query.
     */
    private static final String DEFAULT_METADATA_TYPE = "properties";

    /**
     * Global metadata scope. Used by default if the metadata type is "properties".
     */
    private static final String GLOBAL_METADATA_SCOPE = "global";

    /**
     * Enterprise metadata scope. Used by default if the metadata type is not "properties".
     */
    private static final String ENTERPRISE_METADATA_SCOPE = "enterprise";

    /**
     * Default number of entries per page.
     */
    private static final int DEFAULT_ENTRIES_LIMIT = 100;

    /**
     * @see #getID()
     */
    private String id;

    /**
     * @see #getTemplateKey()
     */
    private String templateKey;

    /**
     * @see #getScope()
     */
    private String scope;

    /**
     * @see #getDisplayName()
     */
    private String displayName;

    /**
     * @see #getIsHidden()
     */
    private Boolean isHidden;

    /**
     * @see #getFields()
     */
    private List<Field> fields;

    /**
     * @see #getCopyInstanceOnItemCopy()
     */
    private Boolean copyInstanceOnItemCopy;

    /**
     * Constructs an empty metadata template.
     */
    public MetadataTemplate() {
        super();
    }

    /**
     * Constructs a metadata template from a JSON string.
     *
     * @param json the json encoded metadate template.
     */
    public MetadataTemplate(String json) {
        super(json);
    }

    /**
     * Constructs a metadate template from a JSON object.
     *
     * @param jsonObject the json encoded metadate template.
     */
    MetadataTemplate(JsonObject jsonObject) {
        super(jsonObject);
    }

    /**
     * Creates new metadata template.
     *
     * @param api         the API connection to be used.
     * @param scope       the scope of the object.
     * @param templateKey a unique identifier for the template.
     * @param displayName the display name of the field.
     * @param hidden      whether this template is hidden in the UI.
     * @param fields      the ordered set of fields for the template
     * @return the metadata template returned from the server.
     */
    public static MetadataTemplate createMetadataTemplate(
        BoxAPIConnection api,
        String scope,
        String templateKey,
        String displayName,
        boolean hidden,
        List<Field> fields
    ) {
        return createMetadataTemplate(api, scope, templateKey, displayName, hidden, fields, null);
    }

    /**
     * Creates new metadata template.
     *
     * @param api                    the API connection to be used.
     * @param scope                  the scope of the object.
     * @param templateKey            a unique identifier for the template.
     * @param displayName            the display name of the field.
     * @param hidden                 whether this template is hidden in the UI.
     * @param fields                 the ordered set of fields for the template
     * @param copyInstanceOnItemCopy determines whether the copy operation should copy the metadata along with the item.
     * @return the metadata template returned from the server.
     */
    public static MetadataTemplate createMetadataTemplate(
        BoxAPIConnection api,
        String scope,
        String templateKey,
        String displayName,
        Boolean hidden,
        List<Field> fields,
        Boolean copyInstanceOnItemCopy
    ) {

        JsonObject jsonObject = new JsonObject();
        jsonObject.add("scope", scope);
        jsonObject.add("displayName", displayName);

        if (hidden != null) {
            jsonObject.add("hidden", hidden);
        }

        if (copyInstanceOnItemCopy != null) {
            jsonObject.add("copyInstanceOnItemCopy", copyInstanceOnItemCopy);
        }

        if (templateKey != null) {
            jsonObject.add("templateKey", templateKey);
        }

        JsonArray fieldsArray = new JsonArray();
        if (fields != null && !fields.isEmpty()) {
            for (Field field : fields) {
                JsonObject fieldObj = getFieldJsonObject(field);

                fieldsArray.add(fieldObj);
            }

            jsonObject.add("fields", fieldsArray);
        }

        URL url = METADATA_TEMPLATE_SCHEMA_URL_TEMPLATE.build(api.getBaseURL());
        BoxJSONRequest request = new BoxJSONRequest(api, url, "POST");
        request.setBody(jsonObject.toString());

        try (BoxJSONResponse response = request.send()) {
            JsonObject responseJSON = Json.parse(response.getJSON()).asObject();

            return new MetadataTemplate(responseJSON);
        }
    }

    /**
     * Gets the JsonObject representation of the given field object.
     *
     * @param field represents a template field
     * @return the json object
     */
    private static JsonObject getFieldJsonObject(Field field) {
        JsonObject fieldObj = new JsonObject();
        fieldObj.add("type", field.getType());
        fieldObj.add("key", field.getKey());
        fieldObj.add("displayName", field.getDisplayName());

        String fieldDesc = field.getDescription();
        if (fieldDesc != null) {
            fieldObj.add("description", field.getDescription());
        }

        Boolean fieldIsHidden = field.getIsHidden();
        if (fieldIsHidden != null) {
            fieldObj.add("hidden", field.getIsHidden());
        }

        JsonArray array = new JsonArray();
        List<String> options = field.getOptions();
        if (options != null && !options.isEmpty()) {
            for (String option : options) {
                JsonObject optionObj = new JsonObject();
                optionObj.add("key", option);

                array.add(optionObj);
            }
            fieldObj.add("options", array);
        }

        return fieldObj;
    }

    /**
     * Updates the schema of an existing metadata template.
     *
     * @param api             the API connection to be used
     * @param scope           the scope of the object
     * @param template        Unique identifier of the template
     * @param fieldOperations the fields that needs to be updated / added in the template
     * @return the updated metadata template
     */
    public static MetadataTemplate updateMetadataTemplate(BoxAPIConnection api, String scope, String template,
                                                          List<FieldOperation> fieldOperations) {

        JsonArray array = new JsonArray();

        for (FieldOperation fieldOperation : fieldOperations) {
            JsonObject jsonObject = getFieldOperationJsonObject(fieldOperation);
            array.add(jsonObject);
        }

        URL url = METADATA_TEMPLATE_URL_TEMPLATE.buildAlpha(api.getBaseURL(), scope, template);
        BoxJSONRequest request = new BoxJSONRequest(api, url, "PUT");
        request.setBody(array.toString());

        try (BoxJSONResponse response = request.send()) {
            JsonObject responseJson = Json.parse(response.getJSON()).asObject();

            return new MetadataTemplate(responseJson);
        }
    }

    /**
     * Deletes the schema of an existing metadata template.
     *
     * @param api      the API connection to be used
     * @param scope    the scope of the object
     * @param template Unique identifier of the template
     */
    public static void deleteMetadataTemplate(BoxAPIConnection api, String scope, String template) {
        URL url = METADATA_TEMPLATE_URL_TEMPLATE.buildAlpha(api.getBaseURL(), scope, template);
        BoxAPIRequest request = new BoxAPIRequest(api, url, "DELETE");
        request.send().close();
    }

    /**
     * Executes a metadata query.
     *
     * @param api       The API connection to be used
     * @param queryBody The query
     * @return An iterable of BoxItem.Info search results
     */
    public static BoxResourceIterable<BoxItem.Info> executeMetadataQuery(
        final BoxAPIConnection api,
        final MetadataQuery queryBody
    ) {

        URL url = METADATA_QUERIES_URL_TEMPLATE.build(api.getBaseURL());
        return new BoxResourceIterable<BoxItem.Info>(
            api, url, queryBody.getLimit(), queryBody.toJsonObject(), queryBody.getMarker()
        ) {

            @Override
            protected BoxItem.Info factory(JsonObject jsonObject) {
                String type = jsonObject.get("type").asString();
                String id = jsonObject.get("id").asString();

                BoxItem.Info nextItemInfo;
                switch (type) {
                    case "folder":
                        BoxFolder folder = new BoxFolder(api, id);
                        nextItemInfo = folder.new Info(jsonObject);
                        break;
                    case "file":
                        BoxFile file = new BoxFile(api, id);
                        nextItemInfo = file.new Info(jsonObject);
                        break;
                    case "web_link":
                        BoxWebLink link = new BoxWebLink(api, id);
                        nextItemInfo = link.new Info(jsonObject);
                        break;
                    default:
                        assert false : "Unsupported item type: " + type;
                        throw new BoxAPIException("Unsupported item type: " + type);
                }

                return nextItemInfo;
            }
        };
    }

    /**
     * Gets the JsonObject representation of the Field Operation.
     *
     * @param fieldOperation represents the template update operation
     * @return the json object
     */
    private static JsonObject getFieldOperationJsonObject(FieldOperation fieldOperation) {
        JsonObject jsonObject = new JsonObject();
        jsonObject.add("op", fieldOperation.getOp().toString());

        String fieldKey = fieldOperation.getFieldKey();
        if (fieldKey != null) {
            jsonObject.add("fieldKey", fieldKey);
        }

        Field field = fieldOperation.getData();
        if (field != null) {
            JsonObject fieldObj = new JsonObject();

            String type = field.getType();
            if (type != null) {
                fieldObj.add("type", type);
            }

            String key = field.getKey();
            if (key != null) {
                fieldObj.add("key", key);
            }

            String displayName = field.getDisplayName();
            if (displayName != null) {
                fieldObj.add("displayName", displayName);
            }

            String description = field.getDescription();
            if (description != null) {
                fieldObj.add("description", description);
            }

            Boolean hidden = field.getIsHidden();
            if (hidden != null) {
                fieldObj.add("hidden", hidden);
            }

            List<String> options = field.getOptions();
            if (options != null) {
                JsonArray array = new JsonArray();
                for (String option : options) {
                    JsonObject optionObj = new JsonObject();
                    optionObj.add("key", option);

                    array.add(optionObj);
                }

                fieldObj.add("options", array);
            }

            Boolean copyInstanceOnItemCopy = field.getCopyInstanceOnItemCopy();
            if (copyInstanceOnItemCopy != null) {
                fieldObj.add("copyInstanceOnItemCopy", copyInstanceOnItemCopy);
            }

            jsonObject.add("data", fieldObj);
        }

        List<String> fieldKeys = fieldOperation.getFieldKeys();
        if (fieldKeys != null) {
            jsonObject.add("fieldKeys", getJsonArray(fieldKeys));
        }

        List<String> enumOptionKeys = fieldOperation.getEnumOptionKeys();
        if (enumOptionKeys != null) {
            jsonObject.add("enumOptionKeys", getJsonArray(enumOptionKeys));
        }

        String enumOptionKey = fieldOperation.getEnumOptionKey();
        if (enumOptionKey != null) {
            jsonObject.add("enumOptionKey", enumOptionKey);
        }

        String multiSelectOptionKey = fieldOperation.getMultiSelectOptionKey();
        if (multiSelectOptionKey != null) {
            jsonObject.add("multiSelectOptionKey", multiSelectOptionKey);
        }

        List<String> multiSelectOptionKeys = fieldOperation.getMultiSelectOptionKeys();
        if (multiSelectOptionKeys != null) {
            jsonObject.add("multiSelectOptionKeys", getJsonArray(multiSelectOptionKeys));
        }

        return jsonObject;
    }

    /**
     * Gets the Json Array representation of the given list of strings.
     *
     * @param keys List of strings
     * @return the JsonArray represents the list of keys
     */
    private static JsonArray getJsonArray(List<String> keys) {
        JsonArray array = new JsonArray();
        for (String key : keys) {
            array.add(key);
        }

        return array;
    }

    /**
     * Gets the metadata template of properties.
     *
     * @param api the API connection to be used.
     * @return the metadata template returned from the server.
     */
    public static MetadataTemplate getMetadataTemplate(BoxAPIConnection api) {
        return getMetadataTemplate(api, DEFAULT_METADATA_TYPE);
    }

    /**
     * Gets the metadata template of specified template type.
     *
     * @param api          the API connection to be used.
     * @param templateName the metadata template type name.
     * @return the metadata template returned from the server.
     */
    public static MetadataTemplate getMetadataTemplate(BoxAPIConnection api, String templateName) {
        String scope = scopeBasedOnType(templateName);
        return getMetadataTemplate(api, templateName, scope);
    }

    /**
     * Gets the metadata template of specified template type.
     *
     * @param api          the API connection to be used.
     * @param templateName the metadata template type name.
     * @param scope        the metadata template scope (global or enterprise).
     * @param fields       the fields to retrieve.
     * @return the metadata template returned from the server.
     */
    public static MetadataTemplate getMetadataTemplate(
        BoxAPIConnection api, String templateName, String scope, String... fields) {
        QueryStringBuilder builder = new QueryStringBuilder();
        if (fields.length > 0) {
            builder.appendParam("fields", fields);
        }
        URL url = METADATA_TEMPLATE_URL_TEMPLATE.buildAlphaWithQuery(
            api.getBaseURL(), builder.toString(), scope, templateName);
        BoxJSONRequest request = new BoxJSONRequest(api, url, "GET");
        try (BoxJSONResponse response = request.send()) {
            return new MetadataTemplate(response.getJSON());
        }
    }

    /**
     * Geta the specified metadata template by its ID.
     *
     * @param api        the API connection to be used.
     * @param templateID the ID of the template to get.
     * @return the metadata template object.
     */
    public static MetadataTemplate getMetadataTemplateByID(BoxAPIConnection api, String templateID) {

        URL url = METADATA_TEMPLATE_BY_ID_URL_TEMPLATE.buildAlpha(api.getBaseURL(), templateID);
        BoxJSONRequest request = new BoxJSONRequest(api, url, "GET");
        try (BoxJSONResponse response = request.send()) {
            return new MetadataTemplate(response.getJSON());
        }
    }

    /**
     * Returns all metadata templates within a user's enterprise.
     *
     * @param api    the API connection to be used.
     * @param fields the fields to retrieve.
     * @return the metadata template returned from the server.
     */
    public static Iterable<MetadataTemplate> getEnterpriseMetadataTemplates(BoxAPIConnection api, String... fields) {
        return getEnterpriseMetadataTemplates(ENTERPRISE_METADATA_SCOPE, api, fields);
    }

    /**
     * Returns all metadata templates within a user's scope. Currently only the enterprise scope is supported.
     *
     * @param scope  the scope of the metadata templates.
     * @param api    the API connection to be used.
     * @param fields the fields to retrieve.
     * @return the metadata template returned from the server.
     */
    public static Iterable<MetadataTemplate> getEnterpriseMetadataTemplates(
        String scope, BoxAPIConnection api, String... fields
    ) {
        return getEnterpriseMetadataTemplates(ENTERPRISE_METADATA_SCOPE, DEFAULT_ENTRIES_LIMIT, api, fields);
    }

    /**
     * Returns all metadata templates within a user's scope. Currently only the enterprise scope is supported.
     *
     * @param scope  the scope of the metadata templates.
     * @param limit  maximum number of entries per response.
     * @param api    the API connection to be used.
     * @param fields the fields to retrieve.
     * @return the metadata template returned from the server.
     */
    public static Iterable<MetadataTemplate> getEnterpriseMetadataTemplates(
        String scope, int limit, BoxAPIConnection api, String... fields) {
        QueryStringBuilder builder = new QueryStringBuilder();
        if (fields.length > 0) {
            builder.appendParam("fields", fields);
        }
        return new BoxResourceIterable<MetadataTemplate>(
            api, ENTERPRISE_METADATA_URL_TEMPLATE.buildAlphaWithQuery(
            api.getBaseURL(), builder.toString(), scope), limit) {

            @Override
            protected MetadataTemplate factory(JsonObject jsonObject) {
                return new MetadataTemplate(jsonObject);
            }
        };
    }

    /**
     * Determines the metadata scope based on type.
     *
     * @param typeName type of the metadata.
     * @return scope of the metadata.
     */
    private static String scopeBasedOnType(String typeName) {
        return typeName.equals(DEFAULT_METADATA_TYPE) ? GLOBAL_METADATA_SCOPE : ENTERPRISE_METADATA_SCOPE;
    }

    /**
     * Gets the ID of the template.
     *
     * @return the template ID.
     */
    public String getID() {
        return this.id;
    }

    /**
     * Gets the unique template key to identify the metadata template.
     *
     * @return the unique template key to identify the metadata template.
     */
    public String getTemplateKey() {
        return this.templateKey;
    }

    /**
     * Gets the metadata template scope.
     *
     * @return the metadata template scope.
     */
    public String getScope() {
        return this.scope;
    }

    /**
     * Gets the displayed metadata template name.
     *
     * @return the displayed metadata template name.
     */
    public String getDisplayName() {
        return this.displayName;
    }

    /**
     * Gets is the metadata template hidden.
     *
     * @return is the metadata template hidden.
     */
    public Boolean getIsHidden() {
        return this.isHidden;
    }

    /**
     * Gets the iterable with all fields the metadata template contains.
     *
     * @return the iterable with all fields the metadata template contains.
     */
    public List<Field> getFields() {
        return this.fields;
    }

    /**
     * Gets whether the copy operation should copy the metadata along with the item.
     *
     * @return whether the copy operation should copy the metadata along with the item.
     */
    public Boolean getCopyInstanceOnItemCopy() {
        return this.copyInstanceOnItemCopy;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    void parseJSONMember(JsonObject.Member member) {
        JsonValue value = member.getValue();
        String memberName = member.getName();
        switch (memberName) {
            case "templateKey":
                this.templateKey = value.asString();
                break;
            case "scope":
                this.scope = value.asString();
                break;
            case "displayName":
                this.displayName = value.asString();
                break;
            case "hidden":
                this.isHidden = value.asBoolean();
                break;
            case "fields":
                this.fields = new ArrayList<>();
                for (JsonValue field : value.asArray()) {
                    this.fields.add(new Field(field.asObject()));
                }
                break;
            case "id":
                this.id = value.asString();
                break;
            case "copyInstanceOnItemCopy":
                this.copyInstanceOnItemCopy = value.asBoolean();
                break;
            default:
                break;
        }
    }

    /**
     * Possible template operations.
     */
    public enum Operation {

        /**
         * Adds an enum option at the end of the enum option list for the specified field.
         */
        addEnumOption,

        /**
         * Edits the enum option.
         */
        editEnumOption,

        /**
         * Removes the specified enum option from the specified enum field.
         */
        removeEnumOption,

        /**
         * Adds a field at the end of the field list for the template.
         */
        addField,

        /**
         * Edits any number of the base properties of a field: displayName, hidden, description.
         */
        editField,

        /**
         * Removes the specified field from the template.
         */
        removeField,

        /**
         * Edits any number of the base properties of a template: displayName, hidden.
         */
        editTemplate,

        /**
         * Reorders the enum option list to match the requested enum option list.
         */
        reorderEnumOptions,

        /**
         * Reorders the field list to match the requested field list.
         */
        reorderFields,

        /**
         * Adds a new option to a multiselect field.
         */
        addMultiSelectOption,

        /**
         * Edits an existing option in a multiselect field.
         */
        editMultiSelectOption,

        /**
         * Removes an option from a multiselect field.
         */
        removeMultiSelectOption,

        /**
         * Changes the display order of options in a multiselect field.
         */
        reorderMultiSelectOptions
    }

    /**
     * Class contains information about the metadata template field.
     */
    public static class Field extends BoxJSONObject {

        /**
         * @see #getID()
         */
        private String id;

        /**
         * @see #getType()
         */
        private String type;

        /**
         * @see #getKey()
         */
        private String key;

        /**
         * @see #getDisplayName()
         */
        private String displayName;

        /**
         * @see #getIsHidden()
         */
        private Boolean isHidden;

        /**
         * @see #getDescription()
         */
        private String description;

        /**
         * @see #getOptionsObjects()
         */
        private List<Option> options;

        /**
         * @see #getCopyInstanceOnItemCopy()
         */
        private Boolean copyInstanceOnItemCopy;

        /**
         * Constructs an empty metadata template.
         */
        public Field() {
            super();
        }

        /**
         * Constructs a metadate template field from a JSON string.
         *
         * @param json the json encoded metadate template field.
         */
        public Field(String json) {
            super(json);
        }

        /**
         * Constructs a metadate template field from a JSON object.
         *
         * @param jsonObject the json encoded metadate template field.
         */
        Field(JsonObject jsonObject) {
            super(jsonObject);
        }

        /**
         * Gets the ID of the template field.
         *
         * @return the template field ID.
         */
        public String getID() {
            return this.id;
        }

        /**
         * Gets the data type of the field's value.
         *
         * @return the data type of the field's value.
         */
        public String getType() {
            return this.type;
        }

        /**
         * Sets the data type of the field's value.
         *
         * @param type the data type of the field's value.
         */
        public void setType(String type) {
            this.type = type;
        }

        /**
         * Gets the key of the field.
         *
         * @return the key of the field.
         */
        public String getKey() {
            return this.key;
        }

        /**
         * Sets the key of the field.
         *
         * @param key the key of the field.
         */
        public void setKey(String key) {
            this.key = key;
        }

        /**
         * Gets the display name of the field.
         *
         * @return the display name of the field.
         */
        public String getDisplayName() {
            return this.displayName;
        }

        /**
         * Sets the display name of the field.
         *
         * @param displayName the display name of the field.
         */
        public void setDisplayName(String displayName) {
            this.displayName = displayName;
        }

        /**
         * Gets is metadata template field hidden.
         *
         * @return is metadata template field hidden.
         */
        public Boolean getIsHidden() {
            return this.isHidden;
        }

        /**
         * Sets is metadata template field hidden.
         *
         * @param isHidden is metadata template field hidden?
         */
        public void setIsHidden(boolean isHidden) {
            this.isHidden = isHidden;
        }

        /**
         * Gets the description of the field.
         *
         * @return the description of the field.
         */
        public String getDescription() {
            return this.description;
        }

        /**
         * Sets the description of the field.
         *
         * @param description the description of the field.
         */
        public void setDescription(String description) {
            this.description = description;
        }

        /**
         * Gets list of possible options for enum type of the field.
         *
         * @return list of possible options for enum type of the field.
         */
        public List<String> getOptions() {
            if (this.options == null) {
                return null;
            }
            List<String> optionsList = new ArrayList<>();
            for (Option option : this.options) {
                optionsList.add(option.getKey());
            }
            return optionsList;
        }

        /**
         * Sets list of possible options for enum type of the field.
         *
         * @param options list of possible options for enum type of the field.
         */
        public void setOptions(List<String> options) {
            if (options == null) {
                this.options = null;
                return;
            }
            List<Option> optionList = new ArrayList<>();
            for (String key : options) {
                JsonObject optionObject = new JsonObject();
                optionObject.add("key", key);
                Option newOption = new Option(optionObject);
                optionList.add(newOption);
            }
            this.options = optionList;
        }

        /**
         * Gets list of possible options for options type of the field.
         *
         * @return list of possible options for option type of the field.
         */
        public List<Option> getOptionsObjects() {
            return this.options;
        }

        /**
         * Gets whether the copy operation should copy the metadata along with the item.
         *
         * @return whether the copy operation should copy the metadata along with the item.
         */
        public Boolean getCopyInstanceOnItemCopy() {
            return this.copyInstanceOnItemCopy;
        }

        /**
         * Sets whether the copy operation should copy the metadata along with the item.
         *
         * @param copyInstanceOnItemCopy whether the copy operation should copy the metadata along with the item.
         */
        public void setCopyInstanceOnItemCopy(Boolean copyInstanceOnItemCopy) {
            this.copyInstanceOnItemCopy = copyInstanceOnItemCopy;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        void parseJSONMember(JsonObject.Member member) {
            JsonValue value = member.getValue();
            String memberName = member.getName();
            switch (memberName) {
                case "type":
                    this.type = value.asString();
                    break;
                case "key":
                    this.key = value.asString();
                    break;
                case "displayName":
                    this.displayName = value.asString();
                    break;
                case "hidden":
                    this.isHidden = value.asBoolean();
                    break;
                case "description":
                    this.description = value.asString();
                    break;
                case "options":
                    this.options = new ArrayList<>();
                    for (JsonValue option : value.asArray()) {
                        this.options.add(new Option(option.asObject()));
                    }
                    break;
                case "id":
                    this.id = value.asString();
                    break;
                case "copyInstanceOnItemCopy":
                    this.copyInstanceOnItemCopy = value.asBoolean();
                    break;
                default:
                    break;
            }
        }
    }

    /**
     * Class contains information about the metadata template option.
     */
    public static class Option extends BoxJSONObject {
        /**
         * @see #getID()
         */
        private String id;
        /**
         * @see #getKey()
         */
        private String key;

        /**
         * Constructs an empty metadata template.
         */
        public Option() {
            super();
        }

        /**
         * Constructs a metadate template option from a JSON string.
         *
         * @param json the json encoded metadata template option.
         */
        public Option(String json) {
            super(json);
        }

        /**
         * Constructs a metadate template option from a JSON object.
         *
         * @param jsonObject the json encoded metadate template option.
         */
        Option(JsonObject jsonObject) {
            super(jsonObject);
        }

        /**
         * Gets the ID of the template field.
         *
         * @return the template field ID.
         */
        public String getID() {
            return this.id;
        }

        /**
         * Gets the key of the field.
         *
         * @return the key of the field.
         */
        public String getKey() {
            return this.key;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        void parseJSONMember(JsonObject.Member member) {
            JsonValue value = member.getValue();
            String memberName = member.getName();
            if (memberName.equals("id")) {
                this.id = value.asString();
            } else if (memberName.equals("key")) {
                this.key = value.asString();
            }
        }
    }

    /**
     * Posssible operations that can be performed in a Metadata template.
     * <ul>
     *     <li>Add an enum option</li>
     *     <li>Edit an enum option</li>
     *     <li>Remove an enum option</li>
     *     <li>Add a field</li>
     *     <li>Edit a field</li>
     *     <li>Remove a field</li>
     *     <li>Edit template</li>
     *     <li>Reorder the enum option</li>
     *     <li>Reorder the field list</li>
     * </ul>
     */
    public static class FieldOperation extends BoxJSONObject {

        private Operation op;
        private Field data;
        private String fieldKey;
        private List<String> fieldKeys;
        private List<String> enumOptionKeys;
        private String enumOptionKey;
        private String multiSelectOptionKey;
        private List<String> multiSelectOptionKeys;

        /**
         * Constructs an empty FieldOperation.
         */
        public FieldOperation() {
            super();
        }

        /**
         * Constructs a Field operation from a JSON string.
         *
         * @param json the json encoded metadate template field.
         */
        public FieldOperation(String json) {
            super(json);
        }

        /**
         * Constructs a Field operation from a JSON object.
         *
         * @param jsonObject the json encoded metadate template field.
         */
        FieldOperation(JsonObject jsonObject) {
            super(jsonObject);
        }

        /**
         * Gets the operation.
         *
         * @return the operation
         */
        public Operation getOp() {
            return this.op;
        }

        /**
         * Sets the operation.
         *
         * @param op the operation
         */
        public void setOp(Operation op) {
            this.op = op;
        }

        /**
         * Gets the data associated with the operation.
         *
         * @return the field object representing the data
         */
        public Field getData() {
            return this.data;
        }

        /**
         * Sets the data.
         *
         * @param data the Field object representing the data
         */
        public void setData(Field data) {
            this.data = data;
        }

        /**
         * Gets the field key.
         *
         * @return the field key
         */
        public String getFieldKey() {
            return this.fieldKey;
        }

        /**
         * Sets the field key.
         *
         * @param fieldKey the key of the field
         */
        public void setFieldKey(String fieldKey) {
            this.fieldKey = fieldKey;
        }

        /**
         * Gets the list of field keys.
         *
         * @return the list of Strings
         */
        public List<String> getFieldKeys() {
            return this.fieldKeys;
        }

        /**
         * Sets the list of the field keys.
         *
         * @param fieldKeys the list of strings
         */
        public void setFieldKeys(List<String> fieldKeys) {
            this.fieldKeys = fieldKeys;
        }

        /**
         * Gets the list of keys of the Enum options.
         *
         * @return the list of Strings
         */
        public List<String> getEnumOptionKeys() {
            return this.enumOptionKeys;
        }

        /**
         * Sets the list of the enum option keys.
         *
         * @param enumOptionKeys the list of Strings
         */
        public void setEnumOptionKeys(List<String> enumOptionKeys) {
            this.enumOptionKeys = enumOptionKeys;
        }

        /**
         * Gets the enum option key.
         *
         * @return the enum option key
         */
        public String getEnumOptionKey() {
            return this.enumOptionKey;
        }

        /**
         * Sets the enum option key.
         *
         * @param enumOptionKey the enum option key
         */
        public void setEnumOptionKey(String enumOptionKey) {
            this.enumOptionKey = enumOptionKey;
        }

        /**
         * Gets the multi-select option key.
         *
         * @return the key.
         */
        public String getMultiSelectOptionKey() {
            return this.multiSelectOptionKey;
        }

        /**
         * Sets the multi-select option key.
         *
         * @param key the key.
         */
        public void setMultiSelectOptionKey(String key) {
            this.multiSelectOptionKey = key;
        }

        /**
         * Gets the list of multiselect option keys.
         *
         * @return the list of keys.
         */
        public List<String> getMultiSelectOptionKeys() {
            return this.multiSelectOptionKeys;
        }

        /**
         * Sets the multi-select option keys.
         *
         * @param keys the list of keys.
         */
        public void setMultiSelectOptionKeys(List<String> keys) {
            this.multiSelectOptionKeys = keys;
        }

        @Override
        public void clearPendingChanges() {
            super.clearPendingChanges();
        }

        /**
         * {@inheritDoc}
         */
        @Override
        void parseJSONMember(JsonObject.Member member) {
            JsonValue value = member.getValue();
            String memberName = member.getName();
            switch (memberName) {
                case "op":
                    this.op = Operation.valueOf(value.asString());
                    break;
                case "data":
                    this.data = new Field(value.asObject());
                    break;
                case "fieldKey":
                    this.fieldKey = value.asString();
                    break;
                case "fieldKeys":
                    if (this.fieldKeys == null) {
                        this.fieldKeys = new ArrayList<>();
                    } else {
                        this.fieldKeys.clear();
                    }
                    for (JsonValue jsonValue : value.asArray()) {
                        this.fieldKeys.add(jsonValue.asString());
                    }
                    break;
                case "enumOptionKeys":
                    if (this.enumOptionKeys == null) {
                        this.enumOptionKeys = new ArrayList<>();
                    } else {
                        this.enumOptionKeys.clear();
                    }

                    for (JsonValue jsonValue : value.asArray()) {
                        this.enumOptionKeys.add(jsonValue.asString());
                    }
                    break;
                case "enumOptionKey":
                    this.enumOptionKey = value.asString();
                    break;
                case "multiSelectOptionKey":
                    this.multiSelectOptionKey = value.asString();
                    break;
                case "multiSelectOptionKeys":
                    this.multiSelectOptionKeys = new ArrayList<>();
                    for (JsonValue key : value.asArray()) {
                        this.multiSelectOptionKeys.add(key.asString());
                    }
                    break;
                default:
                    break;
            }
        }
    }
}
