# coding: utf-8
"""Base classes and helpers for Supervisely API modules."""

import asyncio
from collections import namedtuple
from copy import deepcopy
from math import ceil
from typing import (
    TYPE_CHECKING,
    Any,
    AsyncGenerator,
    Dict,
    List,
    NamedTuple,
    Optional,
    Tuple,
)

import requests

from supervisely._utils import batched, camel_to_snake

if TYPE_CHECKING:
    from supervisely.api.api import Api


class ApiField:
    """Fields for API requests."""

    ID = "id"
    """"""
    NAME = "name"
    """"""
    DESCRIPTION = "description"
    """"""
    CREATED_AT = "createdAt"
    """"""
    UPDATED_AT = "updatedAt"
    """"""
    ROLE = "role"
    """"""
    TEAM_ID = "teamId"
    """"""
    WORKSPACE_ID = "workspaceId"
    """"""
    CONFIG = "config"
    """"""
    SIZE = "size"
    """"""
    PLUGIN_ID = "pluginId"
    """"""
    PLUGIN_VERSION = "pluginVersion"
    """"""
    HASH = "hash"
    """"""
    STATUS = "status"
    """"""
    ONLY_TRAIN = "onlyTrain"
    """"""
    USER_ID = "userId"
    """"""
    README = "readme"
    """"""
    PROJECT_ID = "projectId"
    """"""
    IMAGES_COUNT = "imagesCount"
    """"""
    ITEMS_COUNT = "itemsCount"
    """"""
    DATASET_ID = "datasetId"
    """"""
    LINK = "link"
    """"""
    WIDTH = "width"
    """"""
    HEIGHT = "height"
    """"""
    WEIGHTS_LOCATION = "weightsLocation"
    """"""
    IMAGE_ID = "imageId"
    """"""
    IMAGE_NAME = "imageName"
    """"""
    ANNOTATION = "annotation"
    """"""
    TASK_ID = "taskId"
    """"""
    LABELS_COUNT = "labelsCount"
    """"""
    FILTER = "filter"
    """"""
    FILTERS = "filters"
    """"""
    META = "meta"
    """"""
    FILE_META = "fileMeta"
    """"""
    SHARED_LINK = "sharedLinkToken"
    """"""
    EXPLORE_PATH = "explorePath"
    """"""
    MIME = "mime"
    """"""
    EXT = "ext"
    """"""
    TYPE = "type"
    """"""
    DEFAULT_VERSION = "defaultVersion"
    """"""
    DOCKER_IMAGE = "dockerImage"
    """"""
    CONFIGS = "configs"
    """"""
    VERSIONS = "versions"
    """"""
    VERSION = "version"
    """"""
    TOKEN = "token"
    """"""
    CAPABILITIES = "capabilities"
    """"""
    STARTED_AT = "startedAt"
    """"""
    FINISHED_AT = "finishedAt"
    """"""
    AGENT_ID = "agentId"
    """"""
    MODEL_ID = "modelId"
    """"""
    RESTART_POLICY = "restartPolicy"
    """"""
    SETTINGS = "settings"
    """"""
    SORT = "sort"
    """"""
    SORT_ORDER = "sort_order"
    """"""
    IMAGES = "images"
    """"""
    IMAGE_IDS = "imageIds"
    """"""
    ANNOTATIONS = "annotations"
    """"""
    EMAIL = "email"
    """"""
    LOGIN = "login"
    """"""
    LOGINS = "logins"
    """"""
    DISABLED = "disabled"
    """"""
    DISABLED_AT = "disabledAt"
    """"""
    DISABLED_BY = "disabledBy"
    """"""
    LAST_LOGIN = "lastLogin"
    """"""
    PASSWORD = "password"
    """"""
    ROLE_ID = "roleId"
    """"""
    IS_RESTRICTED = "isRestricted"
    """"""
    DISABLE = "disable"
    """"""
    TEAMS = "teams"
    """"""
    USER_IDS = "userIds"
    """"""
    PROJECT_NAME = (["projectTitle"], "project_name")
    """"""
    DATASET_NAME = (["datasetTitle"], "dataset_name")
    """"""
    WORKSPACE_NAME = (["workspaceTitle"], "workspace_name")
    """"""
    CREATED_BY_ID = (["createdBy"], "created_by_id")
    """"""
    CREATED_BY_LOGIN = (["managerLogin"], "created_by_login")
    """"""
    CREATED_BY_USER = "createdByUser"
    """"""
    ASSIGNED_TO_ID = (["userId"], "assigned_to_id")
    """"""
    ASSIGNED_TO_LOGIN = (["labelerLogin"], "assigned_to_login")
    """"""
    FINISHED_IMAGES_COUNT = "finishedImagesCount"
    """"""
    PROGRESS_IMAGES_COUNT = "progressImagesCount"
    """"""
    CLASSES_TO_LABEL = (["meta", "classes"], "classes_to_label")
    """"""
    TAGS_TO_LABEL = (["meta", "projectTags"], "tags_to_label")
    """"""
    IMAGES_RANGE = (["meta", "range"], "images_range")
    """"""
    REJECTED_IMAGES_COUNT = "rejectedImagesCount"
    """"""
    ACCEPTED_IMAGES_COUNT = "acceptedImagesCount"
    """"""
    OBJECTS_LIMIT_PER_IMAGE = (["meta", "imageFiguresLimit"], "objects_limit_per_image")
    """"""
    TAGS_LIMIT_PER_IMAGE = (["meta", "imageTagsLimit"], "tags_limit_per_image")
    """"""
    FILTER_IMAGES_BY_TAGS = (["meta", "imageTags"], "filter_images_by_tags")
    """"""
    INCLUDE_IMAGES_WITH_TAGS = ([], "include_images_with_tags")
    """"""
    EXCLUDE_IMAGES_WITH_TAGS = ([], "exclude_images_with_tags")
    """"""
    SIZEB = (["size"], "sizeb")
    """"""
    FRAMES = "frames"
    """"""
    FRAMES_COUNT = (["fileMeta", "framesCount"], "frames_count")
    """"""
    PATH_ORIGINAL = "pathOriginal"
    """"""
    OBJECTS_COUNT = "objectsCount"
    """"""
    FRAMES_TO_TIMECODES = (["fileMeta", "framesToTimecodes"], "frames_to_timecodes")
    """"""
    TAGS = "tags"
    """"""
    VIDEO_ID = "videoId"
    """"""
    FRAME_INDEX = (["meta", "frame"], "frame_index")
    """"""
    LABELING_TOOL = "tool"
    """"""
    GEOMETRY_TYPE = "geometryType"
    """"""
    GEOMETRY = "geometry"
    """"""
    GEOMETRY_META = "geometryMeta"
    """"""
    OBJECT_ID = "objectId"
    """"""
    ANNOTATION_OBJECT_ID = "annotationObjectId"
    """"""
    FRAME = "frame"
    """"""
    # FIGURES_COUNT = (['labelsCount'], 'figures_count')
    """"""
    STREAMS = "streams"
    """"""
    VIDEO_IDS = "videoIds"
    """"""
    FRAME_WIDTH = (["fileMeta", "width"], "frame_width")
    """"""
    FRAME_HEIGHT = (["fileMeta", "height"], "frame_height")
    """"""
    VIDEO_NAME = "videoName"
    """"""
    FRAME_RANGE = "frameRange"
    """"""
    TRACK_ID = "trackId"
    """"""
    PROGRESS = "progress"
    """"""
    CURRENT = "current"
    """"""
    TOTAL = "total"
    """"""
    STOPPED = "stopped"
    """"""
    VIDEOS = "videos"
    """"""
    FILENAME = "filename"
    """"""
    SHAPE = "shape"
    """"""
    COLOR = "color"
    """"""
    CLASS_ID = "classId"
    """"""
    ENTITY_ID = "entityId"
    """"""
    ANNOTATION_OBJECTS = "annotationObjects"
    """"""
    TAG_ID = "tagId"
    """"""
    TAG_IDS = "tagIds"
    """"""
    ERROR = "error"
    """"""
    MESSAGE = "message"
    """"""
    CONTENT = "content"
    """"""
    FIGURES = "figures"
    """"""
    LAYOUT = "layout"
    """"""
    WIDGETS = "widgets"
    """"""
    CLOUD_MIME = (["fileMeta", "mime"], "cloud_mime")
    """"""
    PREVIEW = "preview"
    """"""
    FIGURES_COUNT = "figuresCount"
    """"""
    ANN_OBJECTS_COUNT = (["annotationObjectsCount"], "objects_count")
    """"""
    POINTCLOUD_ID = "pointCloudId"
    """"""
    POINTCLOUD_IDS = "pointCloudIds"
    """"""
    POINTCLOUDS = "pointClouds"
    """"""
    ADVANCED = "advanced"
    """"""
    IGNORE_AGENT = "ignoreAgent"
    """"""
    SCRIPT = "script"
    """"""
    LOGS = "logs"
    """"""
    FILES = "files"
    """"""
    HASHES = "hashes"
    """"""
    SUBTITLE = "subtitle"
    """"""
    COMMAND = "command"
    """"""
    DEFAULT_VALUE = "defaultValue"
    """"""
    TITLE = "title"
    """"""
    AREA = "area"
    """"""
    OPTIONS = "options"
    """"""
    REPORT_ID = "reportId"
    """"""
    WIDGET = "widget"
    """"""
    PAYLOAD = "payload"
    """"""
    FIELD = "field"
    """"""
    FIELDS = "fields"
    """"""
    APPEND = "append"
    """"""
    WITH_CUSTOM_DATA = "withCustomBigData"
    """"""
    PATH = "path"
    """"""
    SESSION_ID = "sessionId"
    """"""
    ACTION = "action"
    """"""
    FIGURE_ID = "figureId"
    """"""
    FIGURE_IDS = "figureIds"
    """"""
    VALUE = "value"
    """"""
    ZOOM_FACTOR = "zoomFactor"
    """"""
    FULL_STORAGE_URL = "fullStorageUrl"
    """"""
    REVIEWER_ID = "reviewerId"
    """"""
    REVIEWER_LOGIN = "reviewerLogin"
    """"""
    RECURSIVE = "recursive"
    """"""
    ECOSYSTEM_ITEM_ID = "moduleId"
    """"""
    APP_ID = "appId"
    """"""
    PROJECT = "project"
    """"""
    OUTPUT = "output"
    """"""
    REFERENCE_IMAGE_URL = "referenceImageUrl"
    """"""
    GENERAL = "general"
    """"""
    ENTITIES = "entities"
    """"""
    STORAGE_PATH = "storagePath"
    """"""
    EXT2 = (["meta", "ext"], "ext")
    """"""
    MIME2 = (["meta", "mime"], "mime")
    """"""
    SIZEB2 = (["meta", "size"], "sizeb")
    """"""
    JOB_ID = "jobId"
    """"""
    DATASETS_COUNT = "datasetsCount"
    """"""
    CUSTOM_DATA = "customData"
    """"""
    CONTEXT = "context"
    """"""
    STATE = "state"
    """"""
    IDS = "ids"
    """"""
    DATE = "date"
    """"""
    PARAMS = "params"
    """"""
    LOG_LEVEL = "logLevel"
    """"""
    APP_VERSION = "appVersion"
    """"""
    IS_BRANCH = "isBranch"
    """"""
    TASK_NAME = "taskName"
    """"""
    PROXY_KEEP_URL = "proxyKeepUrl"
    """"""
    USERS_IDS = "usersIds"
    """"""
    MODULE_ID = "moduleId"
    """"""
    USER_LOGIN = "userLogin"
    """"""
    SLUG = "slug"
    """"""
    IS_SHARED = "isShared"
    """"""
    TASKS = "tasks"
    """"""
    REPO = "repo"
    """"""
    VOLUMES = "volumes"
    """"""
    PROCESSING_PATH = "processingPath"
    """"""
    VOLUME_ID = "volumeId"
    """"""
    VOLUME_SLICES = "volumeSlices"
    """"""
    VOLUME_IDS = "volumeIds"
    """"""
    VOLUME_NAME = "volumeName"
    """"""
    SIZEB3 = (["fileMeta", "size"], "sizeb")
    """"""
    INCLUDE = "include"
    """"""
    CLASSES = "classes"
    """"""
    PROJECT_TAGS = "projectTags"
    """"""
    DATASETS = "datasets"
    """"""
    IMAGES_TAGS = "imagesTags"
    """"""
    ANNOTATION_OBJECTS_TAGS = "annotationObjectsTags"
    """"""
    FIGURES_TAGS = "figuresTags"
    """"""
    REDIRECT_REQUESTS = "redirectRequests"
    """"""
    PROCESSING_PATH = "processingPath"
    """"""
    FORCE_METADATA_FOR_LINKS = "forceMetadataForLinks"
    """"""
    SKIP_VALIDATION = "skipValidation"
    """"""
    PAGINATION_MODE = "pagination_mode"
    """"""
    PER_PAGE = "per_page"
    """"""
    SKIP_BOUNDS_VALIDATION = "skipBoundsValidation"
    """"""
    PATHS = "paths"
    """"""
    PROJECTS = "projects"
    """"""
    URL = "url"
    """"""
    ANN_URL = "annotationsUrl"
    """"""
    ARCHIVE_URL = "archiveUrl"
    """"""
    ANN_ARCHIVE_URL = "annotationsArchiveUrl"
    """"""
    BACKUP_ARCHIVE = "backupArchive"
    """"""
    SKIP_EXPORTED = "skipExported"
    """"""
    FROM = "from"
    """"""
    TO = "to"
    """"""
    DATA = "data"
    """"""
    DURATION = "duration"
    """"""
    RAW_VIDEO_META = "rawVideoMeta"
    """"""
    IS_DIR = "isDir"
    """"""
    FIGURE_CLASS_ID = "figureClassId"
    """"""
    FIGURE_CLASS_TITLE = "figureClassTitle"
    """"""
    TOOL_CLASS_ID = "toolClassId"
    """"""
    TOOL_STATE = "toolState"
    """"""
    OPTION = "option"
    """"""
    DECOMPRESS_BITMAP = "decompressBitmap"
    """"""
    FIGURE_STATE = "figureState"
    """"""
    BITMAP = "bitmap"
    """"""
    ACCOUNT_TYPE = "plan"
    """"""
    SILENT = "silent"
    """"""
    PAGE = "page"
    """"""
    PRESERVE_PROJECT_CARD = "preserveProjectCard"
    """"""
    GPU_INFO = "gpuInfo"
    """"""
    IS_PUBLIC = "isPublic"
    """"""
    USAGE = "usage"
    """"""
    PARENT_ID = "parentId"
    """"""
    SRC_ID = "srcId"
    """"""
    DEST_ID = "destId"
    """"""
    COMPLETE_EXISTING = "completeExisting"
    """"""
    MODE = "mode"
    """"""
    FOLDERS = "folders"
    """"""
    WITH_METADATA = "withMetadata"
    """"""
    LIMIT = "limit"
    """"""
    IMPORT_SETTINGS = "importSettings"
    """"""
    ADVANCED_SETTINGS = "advancedSettings"
    """"""
    LIMIT_BY_WORKSPACE = "limitByWorkspace"
    """"""
    COMMIT_TOKEN = "commitToken"
    """"""
    TEAM_FILE_ID = "teamFileId"
    """"""
    PROJECT_UPDATED_AT = "projectUpdatedAt"
    """"""
    NODE = "node"
    """"""
    OPERATOR = "operator"
    """"""
    SMART_TOOL_INPUT = "smartToolInput"
    """"""
    LABELING_QUEUE_ID = "labelingQueueId"
    """"""
    LABELING_EXAM_ID = "labelingExamId"
    """"""
    IS_PINNED = "isPinned"
    """"""
    EVENTS = "events"
    """"""
    COMMENTS = "comments"
    """"""
    COMMENT = "comment"
    """"""
    ASSIGNEES = "assignees"
    """"""
    LINKS = "links"
    """"""
    IS_LOCAL = "isLocal"
    """"""
    ISSUE_ID = "issueId"
    """"""
    EXTRA = "extra"
    """"""
    FIGURE_IMAGE_ID = "figureImageId"
    """"""
    BINDINGS = "bindings"
    """"""
    TOP = "top"
    """"""
    LEFT = "left"
    """"""
    POSITION = "position"
    """"""
    ANNOTATION_DATA = "annotationData"
    """"""
    JOB_ENTITY_STATUS = "jobEntityStatus"
    """"""
    INTEGER_COORDS = "integerCoords"
    """"""
    HIDDEN = "hidden"
    """"""
    PRIORITY = "priority"
    """"""
    CLEAR_LOCAL_DATA_SOURCE = "clearLocalDataSource"
    """"""
    ONLY_RUNNING = "onlyRunning"
    """"""
    SHOW_DISABLED = "showDisabled"
    """"""
    WITH_SHARED = "withShared"
    """"""
    USE_DIRECT_PROGRESS_MESSAGES = "useDirectProgressMessages"
    """"""
    EXTRA_FIELDS = "extraFields"
    """"""
    CUSTOM_SORT = "customSort"
    """"""
    GROUP_ID = "groupId"
    """"""
    EXPERIMENT = "experiment"
    """"""
    IS_FINISHED = "isFinished"
    """"""
    NON_FINAL_VALUE = "nonFinalValue"
    """"""
    HOTKEY = "hotkey"
    """"""
    RELATED_DATA_ID = "relatedDataId"
    """"""
    DOWNLOAD_ID = "downloadId"
    """"""
    OFFSET_START = "offsetStart"
    """"""
    OFFSET_END = "offsetEnd"
    """"""
    SOURCE_BLOB = "sourceBlob"
    """"""
    JOBS = "jobs"
    """"""
    LABELERS = "labelers"
    """"""
    REVIEWERS = "reviewers"
    """"""
    REVIEWER_IDS = "reviewerIds"
    """"""
    ENTITIES_COUNT = "entitiesCount"
    """"""
    ACCEPTED_COUNT = "acceptedCount"
    """"""
    ANNOTATED_COUNT = "annotatedCount"
    """"""
    IN_PROGRESS_COUNT = "inProgressCount"
    """"""
    PENDING_COUNT = "pendingCount"
    """"""
    QUEUE_META = "queueMeta"
    """"""
    ENTITY_IDS = "entityIds"
    """"""
    COLLECTION_ID = "collectionId"
    """"""
    QUALITY_CHECK_USER_IDS = "qualityCheckUserIds"
    """"""
    EMBEDDINGS = "embeddings"
    """"""
    EMBEDDINGS_ENABLED = "embeddingsEnabled"
    """"""
    EMBEDDINGS_UPDATED_AT = "embeddingsUpdatedAt"
    """"""
    EMBEDDINGS_IN_PROGRESS = "embeddingsInProgress"
    """"""
    AI_SEARCH_KEY = "aiSearchKey"
    """"""
    AI_SEARCH_META = "aiSearchMeta"
    """"""
    ENTITY_ITEMS = "entityItems"
    """"""
    SCORE = "score"
    """"""
    HARD_DELETE = "hardDelete"
    """"""
    THRESHOLD = "threshold"
    """"""
    THRESHOLD_DIRECTION = "thresholdDirection"
    """"""
    METHOD = "method"
    """"""
    PROMPT = "prompt"
    """"""
    UPDATE_STRATEGY = "updateStrategy"
    """"""
    FILE_PATH = "filePath"
    """"""
    FILE_KEY = "fileKey"
    """"""
    LOCAL_ENTITIES_COUNT = "localEntitiesCount"
    """"""
    REMOTE_ENTITIES_COUNT = "remoteEntitiesCount"
    """"""
    RESTRICTED_IMAGE_IDS = "restrictedImageIds"
    """"""
    NUMBER_OF_CLUSTERS = "numberOfClusters"
    """"""
    CLUSTERING_METHOD = "clusteringMethod"
    """"""
    ERROR_MESSAGE = "errorMessage"
    """"""
    UNIQUE_ITEMS = "uniqueItems"
    """"""
    NN_CREATED = "nnCreated"
    """"""
    NN_UPDATED = "nnUpdated"
    """"""
    M_GUIDE_ID = (["meta", "guide"], "guide_id")
    """"""
    GUIDE_ID = "guideId"
    """"""
    SINGLE_SESSION_MODE = "singleSessionMode"


def _get_single_item(items):
    """_get_single_item"""
    if len(items) == 0:
        return None
    if len(items) > 1:
        raise RuntimeError("There are several items with the same name")
    return items[0]


class _JsonConvertibleModule:
    """Base class for all module APIs that can be converted to JSON."""

    def _convert_json_info(self, info: dict, skip_missing=False):
        """
        Convert JSON info to a dictionary.

        :param info: Dictionary with JSON info.
        :type info: dict
        :param skip_missing: Skip missing fields.
        :type skip_missing: bool
        :returns: Dictionary with JSON info.
        :rtype: dict
        """
        raise NotImplementedError()


class ModuleApiBase(_JsonConvertibleModule):
    """Base class for all module APIs."""

    MAX_WAIT_ATTEMPTS = 999
    """ Maximum number of attempts that will be made to wait for a certain condition to be met."""
    WAIT_ATTEMPT_TIMEOUT_SEC = 1
    """Number of seconds for intervals between attempts."""

    @staticmethod
    def info_sequence():
        """Get list of all class field names."""

        raise NotImplementedError()

    @staticmethod
    def info_tuple_name():
        """Get string name of NamedTuple."""
        raise NotImplementedError()

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        try:
            field_names = []
            for name in cls.info_sequence():
                if type(name) is str:
                    field_names.append(camel_to_snake(name))
                elif type(name) is tuple and type(name[1]) is str:
                    field_names.append(name[1])
                else:
                    raise RuntimeError("Can not parse field {!r}".format(name))
            cls.InfoType = namedtuple(cls.info_tuple_name(), field_names)
        except NotImplementedError:
            pass

    def __init__(self, api: "Api"):
        """
        :param api: :class:`~supervisely.api.api.Api` object to use for API connection.
        :type api: :class:`~supervisely.api.api.Api`
        """
        self._api = api

    def _add_sort_param(self, data):
        """
        Add sort parameter to the data.

        :param data: Dictionary with request body info.
        :type data: dict
        :returns: Dictionary with request body info and sort parameter.
        :rtype: dict
        """
        results = deepcopy(data)
        results[ApiField.SORT] = ApiField.ID
        results[ApiField.SORT_ORDER] = "asc"  # @TODO: move to enum
        return results

    def get_list_all_pages(
        self,
        method,
        data,
        progress_cb=None,
        convert_json_info_cb=None,
        limit: int = None,
        return_first_response: bool = False,
    ):
        """
        Get list of all or limited quantity entities from the Supervisely server.

        :param method: Request method name
        :type method: str
        :param data: Dictionary with request body info
        :type data: dict
        :param progress_cb: Function for tracking download progress.
        :type progress_cb: Progress, optional
        :param convert_json_info_cb: Function for convert json info
        :type convert_json_info_cb: Callable, optional
        :param limit: Number of entity to retrieve
        :type limit: int, optional
        :param return_first_response: Specify if return first response
        :type return_first_response: bool, optional
        :returns: List of entities.
        :rtype: List[dict]
        """

        if convert_json_info_cb is None:
            convert_func = self._convert_json_info
        else:
            convert_func = convert_json_info_cb

        if ApiField.SORT not in data:
            data = self._add_sort_param(data)
        first_response = self._api.post(method, data).json()
        total = first_response["total"]
        per_page = first_response["perPage"]
        pages_count = first_response["pagesCount"]

        limit_exceeded = False
        results = first_response["entities"]
        if limit is not None and len(results) > limit:
            limit_exceeded = True

        if progress_cb is not None:
            progress_cb(len(results))
        if (pages_count == 1 and len(results) == total) or limit_exceeded is True:
            pass
        else:
            for page_idx in range(2, pages_count + 1):
                temp_resp = self._api.post(method, {**data, "page": page_idx, "per_page": per_page})
                temp_items = temp_resp.json()["entities"]
                results.extend(temp_items)
                if progress_cb is not None:
                    progress_cb(len(temp_items))
                if limit is not None and len(results) > limit:
                    limit_exceeded = True
                    break

            if len(results) != total and limit is None:
                raise RuntimeError(
                    "Method {!r}: error during pagination, some items are missed".format(method)
                )

        if limit is not None:
            results = results[:limit]
        if return_first_response:
            return [convert_func(item) for item in results], first_response
        return [convert_func(item) for item in results]

    def get_list_all_pages_generator(
        self,
        method,
        data,
        progress_cb=None,
        convert_json_info_cb=None,
        limit: int = None,
        return_first_response: bool = False,
    ):
        """
        This generator function retrieves a list of all or a limited quantity of entities from the Supervisely server, yielding batches of entities as they are retrieved

        :param method: Request method name
        :type method: str
        :param data: Dictionary with request body info
        :type data: dict
        :param progress_cb: Function for tracking download progress.
        :type progress_cb: Progress, optional
        :param convert_json_info_cb: Function for convert json info
        :type convert_json_info_cb: Callable, optional
        :param limit: Number of entity to retrieve
        :type limit: int, optional
        :param return_first_response: Specify if return first response
        :type return_first_response: bool, optional
        """

        if convert_json_info_cb is None:
            convert_func = self._convert_json_info
        else:
            convert_func = convert_json_info_cb

        if ApiField.SORT not in data:
            data = self._add_sort_param(data)
        first_response = self._api.post(method, data).json()
        total = first_response["total"]
        # per_page = first_response["perPage"]
        after = first_response["after"]
        # pages_count = first_response["pagesCount"]

        limit_exceeded = False
        results = first_response["entities"]
        processed = len(results)
        yield [convert_func(item) for item in results]
        if limit is not None and len(results) > limit:
            limit_exceeded = True

        if progress_cb is not None:
            progress_cb(len(results))
        if len(first_response["entities"]) == total or limit_exceeded is True:
            pass
        else:
            while after is not None:
                temp_resp = self._api.post(method, {**data, "after": after}).json()
                after = temp_resp.get("after")
                results = temp_resp["entities"]
                # results.extend(temp_items)
                if progress_cb is not None:
                    progress_cb(len(results))
                processed += len(results)
                yield [convert_func(item) for item in results]
                if limit is not None and processed > limit:
                    limit_exceeded = True
                    break

            if processed != total and limit is None:
                raise RuntimeError(
                    "Method {!r}: error during pagination, some items are missed".format(method)
                )

    @staticmethod
    def _get_info_by_name(get_info_by_filters_fn, name):
        """
        Get information about an entity by its name from the Supervisely server.

        :param get_info_by_filters_fn: Function to get information about an entity by its filters.
        :type get_info_by_filters_fn: Callable
        :param name: Name of the entity.
        :type name: str
        :returns: Information about the entity.
        :rtype: dict
        """
        filters = [{"field": ApiField.NAME, "operator": "=", "value": name}]
        return get_info_by_filters_fn(filters)

    def get_info_by_id(self, id):
        """
        Get information about an entity by its ID from the Supervisely server.

        :param id: ID of the entity.
        :type id: int
        """

        raise NotImplementedError()

    @staticmethod
    def _get_free_name(exist_check_fn, name):
        """
        Get a free name for an entity.

        :param exist_check_fn: Function to check if a name is already used.
        :type exist_check_fn: Callable
        :param name: Name of the entity.
        :type name: str
        :returns: Free name for the entity.
        :rtype: str
        """
        res_title = name
        suffix = 1
        while exist_check_fn(res_title):
            res_title = "{}_{:03d}".format(name, suffix)
            suffix += 1
        return res_title

    def _convert_json_info(self, info: dict, skip_missing=False):
        """
        Convert JSON info to a dictionary.

        :param info: Dictionary with JSON info.
        :type info: dict
        :param skip_missing: Skip missing fields.
        :type skip_missing: bool
        :returns: Dictionary with JSON info.
        :rtype: dict
        """

        def _get_value(dict, field_name, skip_missing):
            if skip_missing is True:
                return dict.get(field_name, None)
            else:
                return dict[field_name]

        if info is None:
            return None
        else:
            field_values = []
            for field_name in self.info_sequence():
                if type(field_name) is str:
                    field_values.append(_get_value(info, field_name, skip_missing))
                elif type(field_name) is tuple:
                    value = None
                    for sub_name in field_name[0]:
                        if value is None:
                            value = _get_value(info, sub_name, skip_missing)
                        else:
                            value = _get_value(value, sub_name, skip_missing)
                    field_values.append(value)
                else:
                    raise RuntimeError("Can not parse field {!r}".format(field_name))
            return self.InfoType(*field_values)

    @classmethod
    def convert_info_to_json(cls, info: NamedTuple) -> Dict:
        """
        Convert information about an entity to a dictionary.

        :param info: Information about the entity.
        :type info: NamedTuple
        :returns: Dictionary with information about the entity.
        :rtype: dict
        """

        def _create_nested_dict(keys, value):
            if len(keys) == 1:
                return {keys[0]: value}
            else:
                return {keys[0]: _create_nested_dict(keys[1:], value)}

        json_info = {}
        for field_name, value in zip(cls.info_sequence(), info):
            if type(field_name) is str:
                json_info[field_name] = value
            elif isinstance(field_name, tuple):
                if len(field_name[0]) == 0:
                    json_info[field_name[1]] = value
                else:
                    json_info.update(_create_nested_dict(field_name[0], value))
            else:
                raise RuntimeError("Can not parse field {!r}".format(field_name))
        return json_info

    def _get_response_by_id(self, id, method, id_field, fields=None):
        """_get_response_by_id"""
        try:
            data = {id_field: id}
            if fields is not None:
                data.update(fields)
            return self._api.post(method, data)
        except requests.exceptions.HTTPError as error:
            if error.response.status_code == 404:
                return None
            else:
                raise error

    def _get_info_by_id(self, id, method, fields=None):
        """_get_info_by_id"""
        response = self._get_response_by_id(id, method, id_field=ApiField.ID, fields=fields)
        return self._convert_json_info(response.json()) if (response is not None) else None

    async def get_list_idx_page_async(
        self,
        method: str,
        data: dict,
    ) -> Tuple[int, List[NamedTuple]]:
        """
        Get the list of items for a given page number.
        Page number is specified in the data dictionary.

        :param method: Method to call for listing items.
        :type method: str
        :param data: Data to pass to the API method.
        :type data: dict
        :returns: List of items.
        :rtype: Tuple[int, List[NamedTuple]]
        """

        response = await self._api.post_async(method, data)
        response_json = response.json()
        entities = response_json.get("entities", [])
        # To avoid empty pages when a filter is applied to the data and the `pagesCount` is less than the number calculated based on the items and `per_page` size.
        # Process `pagesCount` in the main function according to the actual number of pages returned.
        pages_count = response_json.get("pagesCount", None)
        if pages_count is None:
            raise ValueError("Can not determine the number of pages to retrieve.")
        return pages_count, [self._convert_json_info(item) for item in entities]

    async def get_list_page_generator_async(
        self,
        method: str,
        data: dict,
        pages_count: Optional[int] = None,
        semaphore: Optional[List[asyncio.Semaphore]] = None,
    ) -> AsyncGenerator[List[Any], None]:
        """
        Yields list of images in dataset asynchronously page by page.

        :param method: Method to call for listing items.
        :type method: str
        :param data: Data to pass to the API method.
        :type data: dict
        :param pages_count: Preferred number of pages to retrieve if used with a ``per_page`` limit.
                            Will be automatically adjusted if the ``pagesCount`` differs from the requested number.
        :type pages_count: int, optional
        :param semaphore: Semaphore for limiting the number of simultaneous requests.
        :type semaphore: :class:`asyncio.Semaphore`, optional
        :returns: List of images in dataset.
        :rtype: AsyncGenerator[List[:class:`~supervisely.api.image_api.ImageInfo`]]

        :Usage Example:

            .. code-block:: python

                import os
                from dotenv import load_dotenv

                import supervisely as sly

                # Load secrets and create API object from .env file (recommended)
                # Learn more here: https://developer.supervisely.com/getting-started/basics-of-authentication
                if sly.is_development():
                    load_dotenv(os.path.expanduser("~/supervisely.env"))

                api = sly.Api.from_env()

                method = 'images.list'
                data = {'datasetId': 123456}

                loop = sly.utils.get_or_create_event_loop()
                images = loop.run_until_complete(api.image.get_list_generator_async(method, data))
        """

        if semaphore is None:
            semaphore = self._api.get_default_semaphore()

        async def sem_task(task):
            async with semaphore:
                return await task

        if pages_count is None:
            pages_count = 999999  # to avoid range lesser than total pages count
        for page_num in range(1, pages_count + 1):
            if page_num <= pages_count:
                data[ApiField.PAGE] = page_num
                total_pages, items = await sem_task(self.get_list_idx_page_async(method, data))

                # To correct `total_pages` count in case filter is applied
                if page_num == 1:
                    pages_count = total_pages

                yield items
            else:
                break


class ModuleApi(ModuleApiBase):
    """Base class for entities that have a parent object in the system."""

    MAX_WAIT_ATTEMPTS = ModuleApiBase.MAX_WAIT_ATTEMPTS
    """Maximum number of attempts that will be made to wait for a certain condition to be met."""

    WAIT_ATTEMPT_TIMEOUT_SEC = ModuleApiBase.WAIT_ATTEMPT_TIMEOUT_SEC
    """Number of seconds for intervals between attempts."""

    def __init__(self, api):
        """
        :param api: :class:`~supervisely.api.api.Api` object to use for API connection.
        :type api: :class:`~supervisely.api.api.Api`
        """
        super().__init__(api)
        self._api = api

    def get_info_by_name(self, parent_id: int, name: str, fields: List[str] = []):
        """
        Get information about an entity by its name from the Supervisely server.

        :param parent_id: ID of the parent entity.
        :type parent_id: int
        :param name: Name of the entity for which the information is being retrieved.
        :type name: str
        :param fields: The list of api fields which will be returned with the response.
        :type fields: List[str]

        :Usage Example:

            .. code-block:: python

                import os
                from dotenv import load_dotenv

                import supervisely as sly

                # Load secrets and create API object from .env file (recommended)
                # Learn more here: https://developer.supervisely.com/getting-started/basics-of-authentication
                if sly.is_development():
                    load_dotenv(os.path.expanduser("~/supervisely.env"))

                api = sly.Api.from_env()

                dataset_id = 55832
                name = "IMG_0315.jpeg"
                info = api.image.get_info_by_name(dataset_id, name)
                print(info)
                # Output: ImageInfo(id=19369643, name='IMG_0315.jpeg', ...)
        """

        return self._get_info_by_name(
            get_info_by_filters_fn=lambda module_name: self._get_info_by_filters(
                parent_id, module_name, fields
            ),
            name=name,
        )

    def _get_info_by_filters(self, parent_id, filters, fields=[]):
        """_get_info_by_filters"""
        # pylint: disable=too-many-function-args
        from supervisely.api.project_api import ProjectApi

        if type(self) == ProjectApi:
            items = self.get_list(parent_id, filters, fields)
        else:
            items = self.get_list(parent_id, filters)
        return _get_single_item(items)

    def get_list(self, parent_id, filters=None):
        """
        Get list of entities in parent entity with given parent ID.

        :param parent_id: parent ID in Supervisely.
        :type parent_id: int
        :param filters: List of parameters to sort output entities.
        :type filters: List[Dict[str, str]], optional

        :Usage Example:

            .. code-block:: python

                import os
                from dotenv import load_dotenv

                import supervisely as sly

                # Load secrets and create API object from .env file (recommended)
                # Learn more here: https://developer.supervisely.com/getting-started/basics-of-authentication
                if sly.is_development():
                    load_dotenv(os.path.expanduser("~/supervisely.env"))

                api = sly.Api.from_env()

                dataset_id = 55832
                images = api.image.get_list(dataset_id)
                print(images)
                # Output: [
                    ImageInfo(id=19369642, ...)
                    ImageInfo(id=19369643, ...)
                    ImageInfo(id=19369644, ...)
                ]
        """

        raise NotImplementedError()

    def exists(self, parent_id, name):
        """
        Checks if an entity with the given parent_id and name exists

        :param parent_id: ID of the parent entity.
        :type parent_id: int
        :param name: Name of the entity.
        :type name: str
        :returns: Returns True if entity exists, and False if not
        :rtype: bool

        :Usage Example:

            .. code-block:: python

                import os
                from dotenv import load_dotenv

                import supervisely as sly

                # Load secrets and create API object from .env file (recommended)
                # Learn more here: https://developer.supervisely.com/getting-started/basics-of-authentication
                if sly.is_development():
                    load_dotenv(os.path.expanduser("~/supervisely.env"))

                api = sly.Api.from_env()

                name = "IMG_0315.jpeg"
                dataset_id = 55832
                exists = api.image.exists(dataset_id, name)
                print(exists) # True
        """

        return self.get_info_by_name(parent_id, name) is not None

    def get_free_name(self, parent_id, name):
        """
        Generates a free name for an entity with the given parent_id and name.
        Adds an increasing suffix to original name until a unique name is found.

        :param parent_id: ID of the parent entity.
        :type parent_id: int
        :param name: Name of the entity.
        :type name: str
        :returns: Returns free name.
        :rtype: str

        :Usage Example:

            .. code-block:: python

                import os
                from dotenv import load_dotenv

                import supervisely as sly

                # Load secrets and create API object from .env file (recommended)
                # Learn more here: https://developer.supervisely.com/getting-started/basics-of-authentication
                if sly.is_development():
                    load_dotenv(os.path.expanduser("~/supervisely.env"))

                api = sly.Api.from_env()

                name = "IMG_0315.jpeg"
                dataset_id = 55832
                free_name = api.image.get_free_name(dataset_id, name)
                print(free_name) # IMG_0315_001.jpeg
        """

        return self._get_free_name(
            exist_check_fn=lambda module_name: self.exists(parent_id, module_name),
            name=name,
        )

    def _get_effective_new_name(self, parent_id, name, change_name_if_conflict=False):
        """_get_effective_new_name"""
        return self.get_free_name(parent_id, name) if change_name_if_conflict else name


# Base class for entities that do not have a parent object in the system.
class ModuleNoParent(ModuleApiBase):
    """Base API module for entities that are not nested under a parent object (parent_id not required)."""

    def get_info_by_name(self, name):
        """get_info_by_name"""
        return self._get_info_by_name(get_info_by_filters_fn=self._get_info_by_filters, name=name)

    def _get_info_by_filters(self, filters):
        """_get_info_by_filters"""
        items = self.get_list(filters)
        return _get_single_item(items)

    def get_list(self, filters=None):
        """get_list"""
        raise NotImplementedError()

    def exists(self, name):
        """exists"""
        return self.get_info_by_name(name) is not None

    def get_free_name(self, name):
        """get_free_name"""
        return self._get_free_name(
            exist_check_fn=lambda module_name: self.exists(module_name), name=name
        )

    def _get_effective_new_name(self, name, change_name_if_conflict=False):
        """"""
        return self.get_free_name(name) if change_name_if_conflict else name


class CloneableModuleApi(ModuleApi):
    """API for cloning entities."""

    MAX_WAIT_ATTEMPTS = ModuleApiBase.MAX_WAIT_ATTEMPTS
    """Maximum number of attempts that will be made to wait for a certain condition to be met."""

    WAIT_ATTEMPT_TIMEOUT_SEC = ModuleApiBase.WAIT_ATTEMPT_TIMEOUT_SEC
    """Number of seconds for intervals between attempts."""

    def _clone_api_method_name(self):
        """_clone_api_method_name"""
        raise NotImplementedError()

    def _clone(self, clone_type: dict, dst_workspace_id: int, dst_name: str):
        """_clone"""
        response = self._api.post(
            self._clone_api_method_name(),
            {
                **clone_type,
                ApiField.WORKSPACE_ID: dst_workspace_id,
                ApiField.NAME: dst_name,
            },
        )
        return response.json()[ApiField.TASK_ID]

    def clone(self, id: int, dst_workspace_id: int, dst_name: str) -> int:
        """Clones the entity with the given ID to the given workspace with the given name.
        Returns the ID of the task that is created to perform the cloning operation.

        :param id: ID of the entity to clone.
        :type id: int
        :param dst_workspace_id: ID of the workspace to clone to.
        :type dst_workspace_id: int
        :param dst_name: Name of the cloned entity.
        :type dst_name: str
        :returns: Returns the ID of the task that is created to perform the cloning operation.
        :rtype: int

        :Usage Example:

            .. code-block:: python

                import os
                from dotenv import load_dotenv

                import supervisely as sly

                # Load secrets and create API object from .env file (recommended)
                # Learn more here: https://developer.supervisely.com/getting-started/basics-of-authentication
                if sly.is_development():
                    load_dotenv(os.path.expanduser("~/supervisely.env"))

                api = sly.Api.from_env()

                # ID of the entity to clone
                project_id = 123456

                # ID of the destination workspace
                workspace_id = 123456

                # Create a task to clone the project
                task_id = api.project.clone(project_id, workspace_id, "my_cloned_project")

                # Wait until the task is finished
                api.task.wait(task_id, api.task.Status.FINISHED)

                task_info = api.task.get_info_by_id(task_id)

                dst_project_id = task_info["meta"]["output"]["project"]["id"]
                print(f"Cloned project ID: {dst_project_id}")
        """

        return self._clone({ApiField.ID: id}, dst_workspace_id, dst_name)

    def clone_by_shared_link(self, shared_link: str, dst_workspace_id: int, dst_name: str) -> int:
        """Clones the entity with the given shared link to the given workspace with the given name.
        Returns the ID of the task that is created to perform the cloning operation.

        :param shared_link: Shared link of the entity to clone.
        :type shared_link: str
        :param dst_workspace_id: ID of the workspace to clone to.
        :type dst_workspace_id: int
        :param dst_name: Name of the cloned entity.
        :type dst_name: str
        :returns: Returns the ID of the task that is created to perform the cloning operation.
        :rtype: int
        """
        return self._clone({ApiField.SHARED_LINK: shared_link}, dst_workspace_id, dst_name)

    def clone_from_explore(self, explore_path: str, dst_workspace_id: int, dst_name: str) -> int:
        """Clones the entity with the given explore path to the given workspace with the given name.
        Returns the ID of the task that is created to perform the cloning operation.

        :param explore_path: Explore path of the entity to clone.
        :type explore_path: str
        :param dst_workspace_id: ID of the workspace to clone to.
        :type dst_workspace_id: int
        :param dst_name: Name of the cloned entity.
        :type dst_name: str
        :returns: Returns the ID of the task that is created to perform the cloning operation.
        :rtype: int
        """
        return self._clone({ApiField.EXPLORE_PATH: explore_path}, dst_workspace_id, dst_name)

    def get_or_clone_from_explore(self, explore_path, dst_workspace_id, dst_name):
        """get_or_clone_from_explore"""
        if not self.exists(dst_workspace_id, dst_name):
            task_id = self.clone_from_explore(explore_path, dst_workspace_id, dst_name)
            self._api.task.wait(task_id, self._api.task.Status.FINISHED)
        item = self.get_info_by_name(dst_workspace_id, dst_name)
        return item


class ModuleWithStatus:
    """API for getting and raising for status of an entity."""

    def get_status(self, id):
        """get_status"""
        raise NotImplementedError()

    def raise_for_status(self, status):
        """raise_for_status"""
        raise NotImplementedError()


class WaitingTimeExceeded(Exception):
    """Exception raised when waiting for a status of an entity exceeds the timeout."""

    pass


class UpdateableModule(_JsonConvertibleModule):
    """API for updating entities."""

    def __init__(self, api):
        """
        :param api: :class:`~supervisely.api.api.Api` object to use for API connection.
        :type api: :class:`~supervisely.api.api.Api`
        """
        self._api = api

    def _get_update_method(self):
        """Get the method name for updating an entity."""
        raise NotImplementedError()

    def update(self, id, name=None, description=None):
        """Update an entity with the specified ID.

        :param id: ID of the entity to update.
        :type id: int
        :param name: New name of the entity.
        :type name: str, optional
        :param description: New description of the entity.
        :type description: str, optional
        :returns: Entity with updated information.
        :rtype: dict
        """
        if name is None and description is None:
            raise ValueError("'name' or 'description' or both have to be specified")

        body = {ApiField.ID: id}
        if name is not None:
            body[ApiField.NAME] = name
        if description is not None:
            body[ApiField.DESCRIPTION] = description

        response = self._api.post(self._get_update_method(), body)
        return self._convert_json_info(response.json())


class RemoveableModuleApi(ModuleApi):
    """API for removing entities."""

    MAX_WAIT_ATTEMPTS = ModuleApiBase.MAX_WAIT_ATTEMPTS
    """Maximum number of attempts that will be made to wait for a certain condition to be met."""

    WAIT_ATTEMPT_TIMEOUT_SEC = ModuleApiBase.WAIT_ATTEMPT_TIMEOUT_SEC
    """Number of seconds for intervals between attempts."""

    def _remove_api_method_name(self):
        """Get the method name for removing an entity."""
        raise NotImplementedError()

    def remove(self, id):
        """
        Remove an entity with the specified ID from the Supervisely server.

        :param id: Entity ID in Supervisely
        :type id: int
        :returns: None
        :rtype: None
        """
        self._api.post(self._remove_api_method_name(), {ApiField.ID: id})

    def remove_batch(self, ids, progress_cb=None):
        """
        Remove entities with given IDs from the Supervisely server.

        :param ids: IDs of entities in Supervisely.
        :type ids: List[int]
        :param progress_cb: Function for control remove progress.
        :type progress_cb: Callable
        :returns: None
        :rtype: None
        """
        for id in ids:
            self.remove(id)
            if progress_cb is not None:
                progress_cb(1)


class RemoveableBulkModuleApi(ModuleApi):
    """API for removing multiple entities."""

    MAX_WAIT_ATTEMPTS = ModuleApiBase.MAX_WAIT_ATTEMPTS
    """Maximum number of attempts that will be made to wait for a certain condition to be met."""

    WAIT_ATTEMPT_TIMEOUT_SEC = ModuleApiBase.WAIT_ATTEMPT_TIMEOUT_SEC
    """Number of seconds for intervals between attempts."""

    def _remove_batch_api_method_name(self):
        """Get the method name for removing a batch of entities."""
        raise NotImplementedError()

    def _remove_batch_field_name(self):
        """Get the field name for the IDs of the entities to remove."""
        raise NotImplementedError()

    def remove_batch(self, ids, progress_cb=None, batch_size=50):
        """
        Remove entities in batches from the Supervisely server.
        All entity IDs must belong to the same nesting (for example team, or workspace, or project, or dataset).
        Therefore, it is necessary to sort IDs before calling this method.

        :param ids: IDs of entities in Supervisely.
        :type ids: List[int]
        :param progress_cb: Function for control remove progress.
        :type progress_cb: Callable

        :Usage Example:

            .. code-block:: python

                import os
                from dotenv import load_dotenv

                import supervisely as sly

                # Load secrets and create API object from .env file (recommended)
                # Learn more here: https://developer.supervisely.com/getting-started/basics-of-authentication
                if sly.is_development():
                    load_dotenv(os.path.expanduser("~/supervisely.env"))

                api = sly.Api.from_env()

                image_ids = [19369645, 19369646, 19369647]
                api.image.remove_batch(image_ids)
        """
        for ids_batch in batched(ids, batch_size=batch_size):
            self._api.post(
                self._remove_batch_api_method_name(),
                {self._remove_batch_field_name(): ids_batch},
            )
            if progress_cb is not None:
                progress_cb(len(ids_batch))

    def remove(self, id):
        """
        Remove an entity with the specified ID from the Supervisely server.

        :param id: Entity ID in Supervisely.
        :type id: int

        :Usage Example:

            .. code-block:: python

                import os
                from dotenv import load_dotenv

                import supervisely as sly

                # Load secrets and create API object from .env file (recommended)
                # Learn more here: https://developer.supervisely.com/getting-started/basics-of-authentication
                if sly.is_development():
                    load_dotenv(os.path.expanduser("~/supervisely.env"))

                api = sly.Api.from_env()

                image_id = 19369643
                api.image.remove(image_id)
        """
        self.remove_batch([id])
