/*
 * Copyright 2015-2020 Austin Keener, Michael Ritter, Florian Spieß, and the JDA contributors
 *
 * 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 net.dv8tion.jda.api.requests.restaction;

import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.Region;
import net.dv8tion.jda.api.entities.ChannelType;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Icon;
import net.dv8tion.jda.api.requests.RestAction;
import net.dv8tion.jda.api.utils.data.DataObject;
import net.dv8tion.jda.api.utils.data.SerializableData;
import net.dv8tion.jda.internal.requests.restaction.GuildActionImpl;
import net.dv8tion.jda.internal.requests.restaction.PermOverrideData;
import net.dv8tion.jda.internal.utils.Checks;

import javax.annotation.CheckReturnValue;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.awt.*;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.BooleanSupplier;

/**
 * {@link net.dv8tion.jda.api.requests.RestAction RestAction} extension
 * specifically designed to allow for the creation of {@link net.dv8tion.jda.api.entities.Guild Guilds}.
 * <br>This is available to all account types but may undergo certain restrictions by Discord.
 *
 * @since  3.4.0
 *
 * @see    net.dv8tion.jda.api.JDA#createGuild(String)
 */
public interface GuildAction extends RestAction<Void>
{
    @Nonnull
    @Override
    GuildAction setCheck(@Nullable BooleanSupplier checks);

    @Nonnull
    @Override
    GuildAction timeout(long timeout, @Nonnull TimeUnit unit);

    @Nonnull
    @Override
    GuildAction deadline(long timestamp);

    /**
     * Sets the voice {@link net.dv8tion.jda.api.Region Region} of
     * the resulting {@link net.dv8tion.jda.api.entities.Guild Guild}.
     *
     * @param  region
     *         The {@link net.dv8tion.jda.api.Region Region} to use
     *
     * @throws java.lang.IllegalArgumentException
     *         If the provided region is a VIP region as per {@link net.dv8tion.jda.api.Region#isVip() Region.isVip()}
     *
     * @return The current GuildAction for chaining convenience
     */
    @Nonnull
    @CheckReturnValue
    GuildAction setRegion(@Nullable Region region);

    /**
     * Sets the {@link net.dv8tion.jda.api.entities.Icon Icon}
     * for the resulting {@link net.dv8tion.jda.api.entities.Guild Guild}
     *
     * @param  icon
     *         The {@link net.dv8tion.jda.api.entities.Icon Icon} to use
     *
     * @return The current GuildAction for chaining convenience
     */
    @Nonnull
    @CheckReturnValue
    GuildAction setIcon(@Nullable Icon icon);

    /**
     * Sets the name for the resulting {@link net.dv8tion.jda.api.entities.Guild Guild}
     *
     * @param  name
     *         The name to use
     *
     * @throws java.lang.IllegalArgumentException
     *         If the provided name is {@code null}, blank or not between 2-100 characters long
     *
     * @return The current GuildAction for chaining convenience
     */
    @Nonnull
    @CheckReturnValue
    GuildAction setName(@Nonnull String name);

    /**
     * Sets the {@link net.dv8tion.jda.api.entities.Guild.VerificationLevel VerificationLevel}
     * for the resulting {@link net.dv8tion.jda.api.entities.Guild Guild}
     *
     * @param  level
     *         The {@link net.dv8tion.jda.api.entities.Guild.VerificationLevel VerificationLevel} to use
     *
     * @return The current GuildAction for chaining convenience
     */
    @Nonnull
    @CheckReturnValue
    GuildAction setVerificationLevel(@Nullable Guild.VerificationLevel level);

    /**
     * Sets the {@link net.dv8tion.jda.api.entities.Guild.NotificationLevel NotificationLevel}
     * for the resulting {@link net.dv8tion.jda.api.entities.Guild Guild}
     *
     * @param  level
     *         The {@link net.dv8tion.jda.api.entities.Guild.NotificationLevel NotificationLevel} to use
     *
     * @return The current GuildAction for chaining convenience
     */
    @Nonnull
    @CheckReturnValue
    GuildAction setNotificationLevel(@Nullable Guild.NotificationLevel level);

    /**
     * Sets the {@link net.dv8tion.jda.api.entities.Guild.ExplicitContentLevel ExplicitContentLevel}
     * for the resulting {@link net.dv8tion.jda.api.entities.Guild Guild}
     *
     * @param  level
     *         The {@link net.dv8tion.jda.api.entities.Guild.ExplicitContentLevel ExplicitContentLevel} to use
     *
     * @return The current GuildAction for chaining convenience
     */
    @Nonnull
    @CheckReturnValue
    GuildAction setExplicitContentLevel(@Nullable Guild.ExplicitContentLevel level);

    /**
     * Adds a {@link net.dv8tion.jda.api.entities.GuildChannel GuildChannel} to the resulting
     * Guild. This cannot be of type {@link net.dv8tion.jda.api.entities.ChannelType#CATEGORY CATEGORY}!
     *
     * @param  channel
     *         The {@link ChannelData ChannelData}
     *         to use for the construction of the GuildChannel
     *
     * @throws java.lang.IllegalArgumentException
     *         If the provided channel is {@code null}!
     *
     * @return The current GuildAction for chaining convenience
     */
    @Nonnull
    @CheckReturnValue
    GuildAction addChannel(@Nonnull ChannelData channel);

    /**
     * Gets the {@link ChannelData ChannelData}
     * of the specified index. The index is 0 based on insertion order of {@link #addChannel(ChannelData)}!
     *
     * @param  index
     *         The 0 based index of the channel
     *
     * @throws java.lang.IndexOutOfBoundsException
     *         If the provided index is not in bounds
     *
     * @return The current GuildAction for chaining convenience
     */
    @Nonnull
    @CheckReturnValue
    ChannelData getChannel(int index);

    /**
     * Removes the {@link ChannelData ChannelData}
     * at the specified index and returns the removed object.
     *
     * @param  index
     *         The index of the channel
     *
     * @throws java.lang.IndexOutOfBoundsException
     *         If the index is out of bounds
     *
     * @return The removed object
     */
    @Nonnull
    @CheckReturnValue
    ChannelData removeChannel(int index);

    /**
     * Removes the provided {@link ChannelData ChannelData}
     * from this GuildAction if present.
     *
     * @param  data
     *         The ChannelData to remove
     *
     * @return The current GuildAction for chaining convenience
     */
    @Nonnull
    @CheckReturnValue
    GuildAction removeChannel(@Nonnull ChannelData data);

    /**
     * Creates a new {@link ChannelData ChannelData}
     * instance and adds it to this GuildAction.
     *
     * @param  type
     *         The {@link net.dv8tion.jda.api.entities.ChannelType ChannelType} of the resulting GuildChannel
     *         <br>This may be of type {@link net.dv8tion.jda.api.entities.ChannelType#TEXT TEXT} or {@link net.dv8tion.jda.api.entities.ChannelType#VOICE VOICE}!
     * @param  name
     *         The name of the channel. This must be alphanumeric with underscores for type TEXT
     *
     * @throws java.lang.IllegalArgumentException
     *         <ul>
     *             <li>If provided with an invalid ChannelType</li>
     *             <li>If the provided name is {@code null} or blank</li>
     *             <li>If the provided name is not between 2-100 characters long</li>
     *             <li>If the type is TEXT and the provided name is not alphanumeric with underscores</li>
     *         </ul>
     *
     * @return The new ChannelData instance
     */
    @Nonnull
    @CheckReturnValue
    ChannelData newChannel(@Nonnull ChannelType type, @Nonnull String name);

    /**
     * Retrieves the {@link RoleData RoleData} for the
     * public role ({@link net.dv8tion.jda.api.entities.Guild#getPublicRole() Guild.getPublicRole()}) for the resulting Guild.
     * <br>The public role is also known in the official client as the {@code @everyone} role.
     *
     * <p><b>You can only change the permissions of the public role!</b>
     *
     * @return RoleData of the public role
     */
    @Nonnull
    @CheckReturnValue
    RoleData getPublicRole();

    /**
     * Retrieves the {@link RoleData RoleData} for the
     * provided index.
     * <br>The public role is at the index 0 and all others are ordered by insertion order!
     *
     * @param  index
     *         The index of the role
     *
     * @throws java.lang.IndexOutOfBoundsException
     *         If the provided index is out of bounds
     *
     * @return RoleData of the provided index
     */
    @Nonnull
    @CheckReturnValue
    RoleData getRole(int index);

    /**
     * Creates and add a new {@link RoleData RoleData} object
     * representing a Role for the resulting Guild.
     *
     * <p>This can be used in {@link GuildAction.ChannelData#addPermissionOverride(GuildAction.RoleData, long, long) ChannelData.addPermissionOverride(...)}.
     * <br>You may change any properties of this {@link RoleData RoleData} instance!
     *
     * @return RoleData for the new Role
     */
    @Nonnull
    @CheckReturnValue
    RoleData newRole();

    /**
     * Mutable object containing information on a {@link net.dv8tion.jda.api.entities.Role Role}
     * of the resulting {@link net.dv8tion.jda.api.entities.Guild Guild} that is constructed by a GuildAction instance
     *
     * <p>This may be used in {@link GuildAction.ChannelData#addPermissionOverride(GuildAction.RoleData, long, long)}  ChannelData.addPermissionOverride(...)}!
     */
    class RoleData implements SerializableData
    {
        protected final long id;
        protected final boolean isPublicRole;

        protected Long permissions;
        protected String name;
        protected Integer color;
        protected Integer position;
        protected Boolean mentionable, hoisted;

        public RoleData(long id)
        {
            this.id = id;
            this.isPublicRole = id == 0;
        }

        /**
         * Sets the raw permission value for this Role
         *
         * @param  rawPermissions
         *         Raw permission value
         *
         * @throws java.lang.IllegalArgumentException
         *         If the provided permissions are negative or exceed the maximum permissions
         *
         * @return The current RoleData instance for chaining convenience
         */
        @Nonnull
        public RoleData setPermissionsRaw(@Nullable Long rawPermissions)
        {
            if (rawPermissions != null)
            {
                Checks.notNegative(rawPermissions, "Raw Permissions");
                Checks.check(rawPermissions <= Permission.ALL_PERMISSIONS, "Provided permissions may not be greater than a full permission set!");
            }
            this.permissions = rawPermissions;
            return this;
        }

        /**
         * Adds the provided permissions to the Role
         *
         * @param  permissions
         *         The permissions to add
         *
         * @throws java.lang.IllegalArgumentException
         *         If any of the provided permissions is {@code null}
         *
         * @return The current RoleData instance for chaining convenience
         */
        @Nonnull
        public RoleData addPermissions(@Nonnull Permission... permissions)
        {
            Checks.notNull(permissions, "Permissions");
            for (Permission perm : permissions)
                Checks.notNull(perm, "Permissions");
            if (this.permissions == null)
                this.permissions = 0L;
            this.permissions |= Permission.getRaw(permissions);
            return this;
        }

        /**
         * Adds the provided permissions to the Role
         *
         * @param  permissions
         *         The permissions to add
         *
         * @throws java.lang.IllegalArgumentException
         *         If any of the provided permissions is {@code null}
         *
         * @return The current RoleData instance for chaining convenience
         */
        @Nonnull
        public RoleData addPermissions(@Nonnull Collection<Permission> permissions)
        {
            Checks.noneNull(permissions, "Permissions");
            if (this.permissions == null)
                this.permissions = 0L;
            this.permissions |= Permission.getRaw(permissions);
            return this;
        }

        /**
         * Sets the name for this Role
         *
         * @param  name
         *         The name
         *
         * @throws java.lang.IllegalStateException
         *         If this is the public role
         *
         * @return The current RoleData instance for chaining convenience
         */
        @Nonnull
        public RoleData setName(@Nullable String name)
        {
            checkPublic("name");
            this.name = name;
            return this;
        }

        /**
         * Sets the color for this Role
         *
         * @param  color
         *         The color for this Role
         *
         * @throws java.lang.IllegalStateException
         *         If this is the public role
         *
         * @return The current RoleData instance for chaining convenience
         */
        @Nonnull
        public RoleData setColor(@Nullable Color color)
        {
            checkPublic("color");
            this.color = color == null ? null : color.getRGB();
            return this;
        }

        /**
         * Sets the color for this Role
         *
         * @param  color
         *         The color for this Role, or {@code null} to unset
         *
         * @throws java.lang.IllegalStateException
         *         If this is the public role
         *
         * @return The current RoleData instance for chaining convenience
         */
        @Nonnull
        public RoleData setColor(@Nullable Integer color)
        {
            checkPublic("color");
            this.color = color;
            return this;
        }

        /**
         * Sets the position for this Role
         *
         * @param  position
         *         The position
         *
         * @throws java.lang.IllegalStateException
         *         If this is the public role
         *
         * @return The current RoleData instance for chaining convenience
         */
        @Nonnull
        public RoleData setPosition(@Nullable Integer position)
        {
            checkPublic("position");
            this.position = position;
            return this;
        }

        /**
         * Sets whether the Role is mentionable
         *
         * @param  mentionable
         *         Whether the role is mentionable
         *
         * @throws java.lang.IllegalStateException
         *         If this is the public role
         *
         * @return The current RoleData instance for chaining convenience
         */
        @Nonnull
        public RoleData setMentionable(@Nullable Boolean mentionable)
        {
            checkPublic("mentionable");
            this.mentionable = mentionable;
            return this;
        }

        /**
         * Sets whether the Role is hoisted
         *
         * @param  hoisted
         *         Whether the role is hoisted
         *
         * @throws java.lang.IllegalStateException
         *         If this is the public role
         *
         * @return The current RoleData instance for chaining convenience
         */
        @Nonnull
        public RoleData setHoisted(@Nullable Boolean hoisted)
        {
            checkPublic("hoisted");
            this.hoisted = hoisted;
            return this;
        }

        @Nonnull
        @Override
        public DataObject toData()
        {
            final DataObject o = DataObject.empty().put("id", Long.toUnsignedString(id));
            if (permissions != null)
                o.put("permissions", permissions);
            if (position != null)
                o.put("position", position);
            if (name != null)
                o.put("name", name);
            if (color != null)
                o.put("color", color & 0xFFFFFF);
            if (mentionable != null)
                o.put("mentionable", mentionable);
            if (hoisted != null)
                o.put("hoist", hoisted);
            return o;
        }

        protected void checkPublic(String comment)
        {
            if (isPublicRole)
                throw new IllegalStateException("Cannot modify " + comment + " for the public role!");
        }
    }

    /**
     * GuildChannel information used for the creation of {@link net.dv8tion.jda.api.entities.GuildChannel Channels} within
     * the construction of a {@link net.dv8tion.jda.api.entities.Guild Guild} via GuildAction.
     *
     * <p>Use with {@link #addChannel(ChannelData) GuildAction.addChannel(ChannelData)}.
     */
    class ChannelData implements SerializableData
    {
        protected final ChannelType type;
        protected final String name;

        protected final Set<PermOverrideData> overrides = new HashSet<>();

        protected Integer position;

        // Text only
        protected String topic;
        protected Boolean nsfw;
        // Voice only
        protected Integer bitrate, userlimit;

        /**
         * Constructs a data object containing information on
         * a {@link net.dv8tion.jda.api.entities.GuildChannel GuildChannel} to be used in the construction
         * of a {@link net.dv8tion.jda.api.entities.Guild Guild}!
         *
         * @param  type
         *         The {@link net.dv8tion.jda.api.entities.ChannelType ChannelType} of the resulting GuildChannel
         *         <br>This may be of type {@link net.dv8tion.jda.api.entities.ChannelType#TEXT TEXT} or {@link net.dv8tion.jda.api.entities.ChannelType#VOICE VOICE}!
         * @param  name
         *         The name of the channel. This must be alphanumeric with underscores for type TEXT
         *
         * @throws java.lang.IllegalArgumentException
         *         <ul>
         *             <li>If provided with an invalid ChannelType</li>
         *             <li>If the provided name is {@code null} or blank</li>
         *             <li>If the provided name is not between 2-100 characters long</li>
         *             <li>If the type is TEXT and the provided name is not alphanumeric with underscores</li>
         *         </ul>
         */
        public ChannelData(ChannelType type, String name)
        {
            Checks.notBlank(name, "Name");
            Checks.check(type == ChannelType.TEXT || type == ChannelType.VOICE, "Can only create channels of type TEXT or VOICE in GuildAction!");
            Checks.check(name.length() >= 2 && name.length() <= 100, "Channel name has to be between 2-100 characters long!");
            Checks.check(type == ChannelType.VOICE || name.matches("[a-zA-Z0-9-_]+"), "Channels of type TEXT must have a name in alphanumeric with underscores!");

            this.type = type;
            this.name = name;
        }

        /**
         * Sets the topic for this channel.
         * <br>These are only relevant to channels of type {@link net.dv8tion.jda.api.entities.ChannelType#TEXT TEXT}.
         *
         * @param  topic
         *         The topic for the channel
         *
         * @throws java.lang.IllegalArgumentException
         *         If the provided topic is bigger than 1024 characters
         *
         * @return This ChannelData instance for chaining convenience
         */
        @Nonnull
        public ChannelData setTopic(@Nullable String topic)
        {
            if (topic != null && topic.length() > 1024)
                throw new IllegalArgumentException("Channel Topic must not be greater than 1024 in length!");
            this.topic = topic;
            return this;
        }

        /**
         * Sets the whether this channel should be marked NSFW.
         * <br>These are only relevant to channels of type {@link net.dv8tion.jda.api.entities.ChannelType#TEXT TEXT}.
         *
         * @param  nsfw
         *         Whether this channel should be marked NSFW
         *
         * @return This ChannelData instance for chaining convenience
         */
        @Nonnull
        public ChannelData setNSFW(@Nullable Boolean nsfw)
        {
            this.nsfw = nsfw;
            return this;
        }

        /**
         * Sets the bitrate for this channel.
         * <br>These are only relevant to channels of type {@link net.dv8tion.jda.api.entities.ChannelType#VOICE VOICE}.
         *
         * @param  bitrate
         *         The bitrate for the channel (8000-96000)
         *
         * @throws java.lang.IllegalArgumentException
         *         If the provided bitrate is not between 8000-96000
         *
         * @return This ChannelData instance for chaining convenience
         */
        @Nonnull
        public ChannelData setBitrate(@Nullable Integer bitrate)
        {
            if (bitrate != null)
            {
                Checks.check(bitrate >= 8000, "Bitrate must be greater than 8000.");
                Checks.check(bitrate <= 96000, "Bitrate must be less than 96000.");
            }
            this.bitrate = bitrate;
            return this;
        }

        /**
         * Sets the userlimit for this channel.
         * <br>These are only relevant to channels of type {@link net.dv8tion.jda.api.entities.ChannelType#VOICE VOICE}.
         *
         * @param  userlimit
         *         The userlimit for the channel (0-99)
         *
         * @throws java.lang.IllegalArgumentException
         *         If the provided userlimit is not between 0-99
         *
         * @return This ChannelData instance for chaining convenience
         */
        @Nonnull
        public ChannelData setUserlimit(@Nullable Integer userlimit)
        {
            if (userlimit != null && (userlimit < 0 || userlimit > 99))
                throw new IllegalArgumentException("Userlimit must be between 0-99!");
            this.userlimit = userlimit;
            return this;
        }

        /**
         * Sets the position for this channel.
         *
         * @param  position
         *         The position for the channel
         *
         * @return This ChannelData instance for chaining convenience
         */
        @Nonnull
        public ChannelData setPosition(@Nullable Integer position)
        {
            this.position = position;
            return this;
        }

        /**
         * Adds a {@link net.dv8tion.jda.api.entities.PermissionOverride PermissionOverride} to this channel
         * with the provided {@link RoleData RoleData}!
         * <br>Use {@link #newRole() GuildAction.newRole()} to retrieve an instance of RoleData.
         *
         * @param  role
         *         The target role
         * @param  allow
         *         The permissions to grant in the override
         * @param  deny
         *         The permissions to deny in the override
         *
         * @throws java.lang.IllegalArgumentException
         *         <ul>
         *             <li>If the provided role is {@code null}</li>
         *             <li>If the provided allow value is negative or exceeds maximum permissions</li>
         *             <li>If the provided deny value is negative or exceeds maximum permissions</li>
         *         </ul>
         *
         * @return This ChannelData instance for chaining convenience
         */
        @Nonnull
        public ChannelData addPermissionOverride(@Nonnull GuildActionImpl.RoleData role, long allow, long deny)
        {
            Checks.notNull(role, "Role");
            Checks.notNegative(allow, "Granted permissions value");
            Checks.notNegative(deny, "Denied permissions value");
            Checks.check(allow <= Permission.ALL_PERMISSIONS, "Specified allow value may not be greater than a full permission set");
            Checks.check(deny <= Permission.ALL_PERMISSIONS,  "Specified deny value may not be greater than a full permission set");
            this.overrides.add(new PermOverrideData(PermOverrideData.ROLE_TYPE, role.id, allow, deny));
            return this;
        }

        /**
         * Adds a {@link net.dv8tion.jda.api.entities.PermissionOverride PermissionOverride} to this channel
         * with the provided {@link GuildAction.RoleData RoleData}!
         * <br>Use {@link #newRole() GuildAction.newRole()} to retrieve an instance of RoleData.
         *
         * @param  role
         *         The target role
         * @param  allow
         *         The permissions to grant in the override
         * @param  deny
         *         The permissions to deny in the override
         *
         * @throws java.lang.IllegalArgumentException
         *         <ul>
         *             <li>If the provided role is {@code null}</li>
         *             <li>If any permission is {@code null}</li>
         *         </ul>
         *
         * @return This ChannelData instance for chaining convenience
         */
        @Nonnull
        public ChannelData addPermissionOverride(@Nonnull GuildActionImpl.RoleData role, @Nullable Collection<Permission> allow, @Nullable Collection<Permission> deny)
        {
            long allowRaw = 0;
            long denyRaw = 0;
            if (allow != null)
            {
                Checks.noneNull(allow, "Granted Permissions");
                allowRaw = Permission.getRaw(allow);
            }
            if (deny != null)
            {
                Checks.noneNull(deny, "Denied Permissions");
                denyRaw = Permission.getRaw(deny);
            }
            return addPermissionOverride(role, allowRaw, denyRaw);
        }

        @Nonnull
        @Override
        public DataObject toData()
        {
            final DataObject o = DataObject.empty();
            o.put("name", name);
            o.put("type", type.getId());
            if (topic != null)
                o.put("topic", topic);
            if (nsfw != null)
                o.put("nsfw", nsfw);
            if (bitrate != null)
                o.put("bitrate", bitrate);
            if (userlimit != null)
                o.put("user_limit", userlimit);
            if (position != null)
                o.put("position", position);
            if (!overrides.isEmpty())
                o.put("permission_overwrites", overrides);
            return o;
        }
    }
}