package com.box.sdk;

import java.net.URL;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.eclipsesource.json.JsonArray;
import com.eclipsesource.json.JsonObject;
import com.eclipsesource.json.JsonValue;

/**
 * The abstract base class for items in a user's file tree (files, folders, etc.).
 */
public abstract class BoxItem extends BoxResource {
    /**
     * An array of all possible file fields that can be requested when calling {@link #getInfo()}.
     */
    public static final String[] ALL_FIELDS = {"type", "id", "sequence_id", "etag", "sha1", "name", "description",
        "size", "path_collection", "created_at", "modified_at", "trashed_at", "purged_at", "content_created_at",
        "content_modified_at", "created_by", "modified_by", "owned_by", "shared_link", "parent", "item_status",
        "version_number", "comment_count", "permissions", "tags", "lock", "extension", "is_package",
        "folder_upload_email", "item_collection", "sync_state", "has_collaborations", "can_non_owners_invite",
        "file_version", "collections", "expires_at"};
    /**
     * Shared Item URL Template.
     */
    public static final URLTemplate SHARED_ITEM_URL_TEMPLATE = new URLTemplate("shared_items");

    /**
     * Url template for operations with watermarks.
     */
    public static final URLTemplate WATERMARK_URL_TEMPLATE = new URLTemplate("/watermark");

    /**
     * Constructs a BoxItem for an item with a given ID.
     * @param  api the API connection to be used by the item.
     * @param  id  the ID of the item.
     */
    public BoxItem(BoxAPIConnection api, String id) {
        super(api, id);
    }

    /**
     * @return URL for the current object, constructed as base URL pus an item specifier.
     */
    protected URL getItemURL() {
        return new URLTemplate("").build(this.getAPI().getBaseURL());
    }

    /**
     * Gets an item that was shared with a shared link.
     * @param  api        the API connection to be used by the shared item.
     * @param  sharedLink the shared link to the item.
     * @return            info about the shared item.
     */
    public static BoxItem.Info getSharedItem(BoxAPIConnection api, String sharedLink) {
        return getSharedItem(api, sharedLink, null);
    }

    /**
     * Gets an item that was shared with a password-protected shared link.
     * @param  api        the API connection to be used by the shared item.
     * @param  sharedLink the shared link to the item.
     * @param  password   the password for the shared link.
     * @return            info about the shared item.
     */
    public static BoxItem.Info getSharedItem(BoxAPIConnection api, String sharedLink, String password) {
        BoxAPIConnection newAPI = new SharedLinkAPIConnection(api, sharedLink, password);
        URL url = SHARED_ITEM_URL_TEMPLATE.build(newAPI.getBaseURL());
        BoxAPIRequest request = new BoxAPIRequest(newAPI, url, "GET");
        BoxJSONResponse response = (BoxJSONResponse) request.send();
        JsonObject json = JsonObject.readFrom(response.getJSON());
        return (BoxItem.Info) BoxResource.parseInfo(newAPI, json);
    }

    /**
     * Used to retrieve the watermark for the item.
     * If the item does not have a watermark applied to it, a 404 Not Found will be returned by API.
     * @param itemUrl url template for the item.
     * @param fields the fields to retrieve.
     * @return the watermark associated with the item.
     */
    protected BoxWatermark getWatermark(URLTemplate itemUrl, String... fields) {
        URL watermarkUrl = itemUrl.build(this.getAPI().getBaseURL(), this.getID());
        QueryStringBuilder builder = new QueryStringBuilder();
        if (fields.length > 0) {
            builder.appendParam("fields", fields);
        }
        URL url = WATERMARK_URL_TEMPLATE.buildWithQuery(watermarkUrl.toString(), builder.toString());
        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "GET");
        BoxJSONResponse response = (BoxJSONResponse) request.send();
        return new BoxWatermark(response.getJSON());
    }

    /**
     * Used to apply or update the watermark for the item.
     * @param itemUrl url template for the item.
     * @param imprint the value must be "default", as custom watermarks is not yet supported.
     * @return the watermark associated with the item.
     */
    protected BoxWatermark applyWatermark(URLTemplate itemUrl, String imprint) {
        URL watermarkUrl = itemUrl.build(this.getAPI().getBaseURL(), this.getID());
        URL url = WATERMARK_URL_TEMPLATE.build(watermarkUrl.toString());
        BoxJSONRequest request = new BoxJSONRequest(this.getAPI(), url, "PUT");
        JsonObject body = new JsonObject()
                .add(BoxWatermark.WATERMARK_JSON_KEY, new JsonObject()
                        .add(BoxWatermark.WATERMARK_IMPRINT_JSON_KEY, imprint));
        request.setBody(body.toString());
        BoxJSONResponse response = (BoxJSONResponse) request.send();
        return new BoxWatermark(response.getJSON());
    }

    /**
     * Removes a watermark from the item.
     * If the item did not have a watermark applied to it, a 404 Not Found will be returned by API.
     * @param itemUrl url template for the item.
     */
    protected void removeWatermark(URLTemplate itemUrl) {
        URL watermarkUrl = itemUrl.build(this.getAPI().getBaseURL(), this.getID());
        URL url = WATERMARK_URL_TEMPLATE.build(watermarkUrl.toString());
        BoxAPIRequest request = new BoxAPIRequest(this.getAPI(), url, "DELETE");
        BoxAPIResponse response = request.send();
        response.disconnect();
    }

    /**
     * Copies this item to another folder.
     * @param  destination the destination folder.
     * @return             info about the copied item.
     */
    public abstract BoxItem.Info copy(BoxFolder destination);

    /**
     * Copies this item to another folder and gives it a new name. If the destination is the same folder as the item's
     * current parent, then newName must be a new, unique name.
     * @param  destination the destination folder.
     * @param  newName     a new name for the copied item.
     * @return             info about the copied item.
     */
    public abstract BoxItem.Info copy(BoxFolder destination, String newName);

    /**
     * Moves this item to another folder.
     * @param  destination the destination folder.
     * @return             info about the moved item.
     */
    public abstract BoxItem.Info move(BoxFolder destination);

    /**
     * Moves this item to another folder and gives it a new name.
     * @param  destination the destination folder.
     * @param  newName     a new name for the moved item.
     * @return             info about the moved item.
     */
    public abstract BoxItem.Info move(BoxFolder destination, String newName);

    /**
     * Creates a new shared link for this item.
     *
     * <p>This method is a convenience method for manually creating a new shared link and applying it to this item with
     * {@link Info#setSharedLink}. You may want to create the shared link manually so that it can be updated along with
     * other changes to the item's info in a single network request, giving a boost to performance.</p>
     *
     * @param  access      the access level of the shared link.
     * @param  unshareDate the date and time at which the link will expire. Can be null to create a non-expiring link.
     * @param  permissions the permissions of the shared link. Can be null to use the default permissions.
     * @return             the created shared link.
     */
    public abstract BoxSharedLink createSharedLink(BoxSharedLink.Access access, Date unshareDate,
        BoxSharedLink.Permissions permissions);

    /**
     * Gets information about this item.
     * @return info about this item.
     */
    public abstract BoxItem.Info getInfo();

    /**
     * Gets information about this item that's limited to a list of specified fields.
     * @param  fields the fields to retrieve.
     * @return        info about this item containing only the specified fields.
     */
    public abstract BoxItem.Info getInfo(String... fields);

    /**
     * Sets the collections that this item belongs to.
     * @param   collections the collections that this item should belong to.
     * @return              info about the item, including the collections it belongs to.
     */
    public abstract BoxItem.Info setCollections(BoxCollection... collections);

    /**
     * Contains information about a BoxItem.
     */
    public abstract class Info extends BoxResource.Info {
        private String type;
        private String sequenceID;
        private String etag;
        private String name;
        private Date createdAt;
        private Date modifiedAt;
        private String description;
        private long size;
        private List<BoxFolder.Info> pathCollection;
        private BoxUser.Info createdBy;
        private BoxUser.Info modifiedBy;
        private Date trashedAt;
        private Date purgedAt;
        private Date contentCreatedAt;
        private Date contentModifiedAt;
        private BoxUser.Info ownedBy;
        private BoxSharedLink sharedLink;
        private List<String> tags;
        private BoxFolder.Info parent;
        private String itemStatus;
        private Date expiresAt;
        private Set<BoxCollection.Info> collections;

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

        /**
         * Constructs an Info object by parsing information from a JSON string.
         * @param  json the JSON string to parse.
         */
        public Info(String json) {
            super(json);
        }

        /**
         * Constructs an Info object using an already parsed JSON object.
         * @param  jsonObject the parsed JSON object.
         */
        Info(JsonObject jsonObject) {
            super(jsonObject);
        }

        /**
         * Gets the item type.
         * @return the item's type.
         */
        public String getType() {
            return this.type;
        }

        /**
         * Gets a unique string identifying the version of the item.
         * @return a unique string identifying the version of the item.
         */
        public String getEtag() {
            return this.etag;
        }

        /**
         * Gets the name of the item.
         * @return the name of the item.
         */
        public String getName() {
            return this.name;
        }

        /**
         * Sets the name of the item.
         * @param name the new name of the item.
         */
        public void setName(String name) {
            this.name = name;
            this.addPendingChange("name", name);
        }

        /**
         * Gets the time the item was created.
         * @return the time the item was created.
         */
        public Date getCreatedAt() {
            return this.createdAt;
        }

        /**
         * Gets the time the item was last modified.
         * @return the time the item was last modified.
         */
        public Date getModifiedAt() {
            return this.modifiedAt;
        }

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

        /**
         * Sets the description of the item.
         * @param description the new description of the item.
         */
        public void setDescription(String description) {
            this.description = description;
            this.addPendingChange("description", description);
        }

        /**
         * Gets the size of the item in bytes.
         * @return the size of the item in bytes.
         */
        public long getSize() {
            return this.size;
        }

        /**
         * Gets the path of folders to the item, starting at the root.
         * @return the path of folders to the item.
         */
        public List<BoxFolder.Info> getPathCollection() {
            return this.pathCollection;
        }

        /**
         * Gets info about the user who created the item.
         * @return info about the user who created the item.
         */
        public BoxUser.Info getCreatedBy() {
            return this.createdBy;
        }

        /**
         * Gets info about the user who last modified the item.
         * @return info about the user who last modified the item.
         */
        public BoxUser.Info getModifiedBy() {
            return this.modifiedBy;
        }

        /**
         * Gets the time that the item was trashed.
         * @return the time that the item was trashed.
         */
        public Date getTrashedAt() {
            return this.trashedAt;
        }

        /**
         * Gets the time that the item was purged from the trash.
         * @return the time that the item was purged from the trash.
         */
        public Date getPurgedAt() {
            return this.purgedAt;
        }

        /**
         * Gets the time that the item was created according to the uploader.
         * @return the time that the item was created according to the uploader.
         */
        public Date getContentCreatedAt() {
            return this.contentCreatedAt;
        }

        /**
         * Gets the time that the item was last modified according to the uploader.
         * @return the time that the item was last modified according to the uploader.
         */
        public Date getContentModifiedAt() {
            return this.contentModifiedAt;
        }

        /**
         * Gets the expires at time for this item.
         * @return the time that the item will expire at.
         */
        public Date getExpiresAt() {
            return this.expiresAt;
        }

        /**
         * Gets info about the user who owns the item.
         * @return info about the user who owns the item.
         */
        public BoxUser.Info getOwnedBy() {
            return this.ownedBy;
        }

        /**
         * Gets the shared link for the item.
         * @return the shared link for the item.
         */
        public BoxSharedLink getSharedLink() {
            return this.sharedLink;
        }

        /**
         * Sets a shared link for the item.
         * @param sharedLink the shared link for the item.
         */
        public void setSharedLink(BoxSharedLink sharedLink) {
            this.removeChildObject("shared_link");
            this.sharedLink = sharedLink;
            this.addChildObject("shared_link", sharedLink);
        }

        /**
         * Removes the shared link for the item.
         */
        public void removeSharedLink() {
            this.addChildObject("shared_link", null);
        }

        /**
         * Gets a unique ID for use with the {@link EventStream}.
         * @return a unique ID for use with the EventStream.
         */
        public String getSequenceID() {
            return this.sequenceID;
        }

        /**
         * Gets a list of all the tags applied to the item.
         *
         * <p>Note that this field isn't populated by default and must be specified as a field parameter when getting
         * Info about the item.</p>
         *
         * @return a list of all the tags applied to the item.
         */
        public List<String> getTags() {
            return this.tags;
        }

        /**
         * Sets the tags for an item.
         * @param tags The new tags for the item.
         */
        public void setTags(List<String> tags) {
            this.tags = tags;
            JsonArray tagsJSON = new JsonArray();
            for (String tag : tags) {
                tagsJSON.add(tag);
            }
            this.addPendingChange("tags", tagsJSON);
        }

        /**
         * Gets info about the parent folder of the item.
         * @return info about the parent folder of the item.
         */
        public BoxFolder.Info getParent() {
            return this.parent;
        }

        /**
         * Gets the status of the item.
         * @return the status of the item.
         */
        public String getItemStatus() {
            return this.itemStatus;
        }

        /**
         * Gets info about the collections that this item belongs to.
         * @return info about the collections that this item belongs to.
         */
        public Iterable<BoxCollection.Info> getCollections() {
            return this.collections;
        }

        /**
         * Sets the collections that this item belongs to.
         * @param collections the new list of collections that this item should belong to.
         */
        public void setCollections(Iterable<BoxCollection> collections) {
            if (this.collections == null) {
                this.collections = new HashSet<BoxCollection.Info>();
            } else {
                this.collections.clear();
            }

            JsonArray jsonArray = new JsonArray();
            for (BoxCollection collection : collections) {
                JsonObject jsonObject = new JsonObject();
                jsonObject.add("id", collection.getID());
                jsonArray.add(jsonObject);
                this.collections.add(collection.new Info());
            }
            this.addPendingChange("collections", jsonArray);
        }

        @Override
        protected void parseJSONMember(JsonObject.Member member) {
            super.parseJSONMember(member);
            JsonValue value = member.getValue();
            String memberName = member.getName();

            try {
                if (memberName.equals("sequence_id")) {
                    this.sequenceID = value.asString();
                } else if (memberName.equals("type")) {
                    this.type = value.asString();
                } else if (memberName.equals("etag")) {
                    this.etag = value.asString();
                } else if (memberName.equals("name")) {
                    this.name = value.asString();
                } else if (memberName.equals("created_at")) {
                    this.createdAt = BoxDateFormat.parse(value.asString());
                } else if (memberName.equals("modified_at")) {
                    this.modifiedAt = BoxDateFormat.parse(value.asString());
                } else if (memberName.equals("description")) {
                    this.description = value.asString();
                } else if (memberName.equals("size")) {
                    this.size = Double.valueOf(value.toString()).longValue();
                } else if (memberName.equals("trashed_at")) {
                    this.trashedAt = BoxDateFormat.parse(value.asString());
                } else if (memberName.equals("purged_at")) {
                    this.purgedAt = BoxDateFormat.parse(value.asString());
                } else if (memberName.equals("content_created_at")) {
                    this.contentCreatedAt = BoxDateFormat.parse(value.asString());
                } else if (memberName.equals("content_modified_at")) {
                    this.contentModifiedAt = BoxDateFormat.parse(value.asString());
                }  else if (memberName.equals("expires_at")) {
                    this.expiresAt = BoxDateFormat.parse(value.asString());
                } else if (memberName.equals("path_collection")) {
                    this.pathCollection = this.parsePathCollection(value.asObject());
                } else if (memberName.equals("created_by")) {
                    this.createdBy = this.parseUserInfo(value.asObject());
                } else if (memberName.equals("modified_by")) {
                    this.modifiedBy = this.parseUserInfo(value.asObject());
                } else if (memberName.equals("owned_by")) {
                    this.ownedBy = this.parseUserInfo(value.asObject());
                } else if (memberName.equals("shared_link")) {
                    if (this.sharedLink == null) {
                        this.setSharedLink(new BoxSharedLink(value.asObject()));
                    } else {
                        this.sharedLink.update(value.asObject());
                    }
                } else if (memberName.equals("tags")) {
                    this.tags = this.parseTags(value.asArray());
                } else if (memberName.equals("parent")) {
                    JsonObject jsonObject = value.asObject();
                    if (this.parent == null) {
                        String id = jsonObject.get("id").asString();
                        BoxFolder parentFolder = new BoxFolder(getAPI(), id);
                        this.parent = parentFolder.new Info(jsonObject);
                    } else {
                        this.parent.update(jsonObject);
                    }
                } else if (memberName.equals("item_status")) {
                    this.itemStatus = value.asString();
                } else if (memberName.equals("collections")) {
                    if (this.collections == null) {
                        this.collections = new HashSet<BoxCollection.Info>();
                    } else {
                        this.collections.clear();
                    }

                    BoxAPIConnection api = getAPI();
                    JsonArray jsonArray = value.asArray();
                    for (JsonValue arrayValue : jsonArray) {
                        JsonObject jsonObject = arrayValue.asObject();
                        String id = jsonObject.get("id").asString();
                        BoxCollection collection = new BoxCollection(api, id);
                        BoxCollection.Info collectionInfo = collection.new Info(jsonObject);
                        this.collections.add(collectionInfo);
                    }
                }
            } catch (Exception e) {
                throw new BoxDeserializationException(memberName, value.toString(), e);
            }
        }

        private List<BoxFolder.Info> parsePathCollection(JsonObject jsonObject) {
            int count = jsonObject.get("total_count").asInt();
            List<BoxFolder.Info> pathCollection = new ArrayList<BoxFolder.Info>(count);
            JsonArray entries = jsonObject.get("entries").asArray();
            for (JsonValue value : entries) {
                JsonObject entry = value.asObject();
                String id = entry.get("id").asString();
                BoxFolder folder = new BoxFolder(getAPI(), id);
                pathCollection.add(folder.new Info(entry));
            }

            return pathCollection;
        }

        private BoxUser.Info parseUserInfo(JsonObject jsonObject) {
            String userID = jsonObject.get("id").asString();
            BoxUser user = new BoxUser(getAPI(), userID);
            return user.new Info(jsonObject);
        }

        private List<String> parseTags(JsonArray jsonArray) {
            List<String> tags = new ArrayList<String>(jsonArray.size());
            for (JsonValue value : jsonArray) {
                tags.add(value.asString());
            }

            return tags;
        }
    }
}