# Copyright Cloudinary
import json
import re
import socket
from os.path import getsize

import cloudinary
import urllib3
import certifi
from cloudinary import utils
from cloudinary.api import Error
from cloudinary.compat import string_types
from urllib3.exceptions import HTTPError
from urllib3 import PoolManager

try:
    from urllib3.contrib.appengine import AppEngineManager, is_appengine_sandbox
except Exception:
    def is_appengine_sandbox():
        return False

try:  # Python 2.7+
    from collections import OrderedDict
except ImportError:
    from urllib3.packages.ordered_dict import OrderedDict

if is_appengine_sandbox():
    # AppEngineManager uses AppEngine's URLFetch API behind the scenes
    _http = AppEngineManager()
else:
    # PoolManager uses a socket-level API behind the scenes
    _http = PoolManager(
        cert_reqs='CERT_REQUIRED',
        ca_certs=certifi.where()
    )


def upload(file, **options):
    params = utils.build_upload_params(**options)
    return call_api("upload", params, file=file, **options)


def unsigned_upload(file, upload_preset, **options):
    return upload(file, upload_preset=upload_preset, unsigned=True, **options)


def upload_image(file, **options):
    result = upload(file, **options)
    return cloudinary.CloudinaryImage(
        result["public_id"], version=str(result["version"]),
        format=result.get("format"), metadata=result)


def upload_resource(file, **options):
    result = upload(file, **options)
    return cloudinary.CloudinaryResource(
        result["public_id"], version=str(result["version"]),
        format=result.get("format"), type=result["type"], resource_type=result["resource_type"], metadata=result)


def upload_large(file, **options):
    """ Upload large files. """
    upload_id = utils.random_public_id()
    with open(file, 'rb') as file_io:
        results = None
        current_loc = 0
        chunk_size = options.get("chunk_size", 20000000)
        file_size = getsize(file)
        chunk = file_io.read(chunk_size)
        while chunk:
            range = "bytes {0}-{1}/{2}".format(current_loc, current_loc + len(chunk) - 1, file_size)
            current_loc += len(chunk)

            results = upload_large_part((file, chunk),
                                       http_headers={"Content-Range": range, "X-Unique-Upload-Id": upload_id},
                                       **options)
            options["public_id"] = results.get("public_id")
            chunk = file_io.read(chunk_size)
        return results


def upload_large_part(file, **options):
    """ Upload large files. """
    params = utils.build_upload_params(**options)
    if 'resource_type' not in options: options['resource_type'] = "raw"
    return call_api("upload", params, file=file, **options)


def destroy(public_id, **options):
    params = {
        "timestamp": utils.now(),
        "type": options.get("type"),
        "invalidate": options.get("invalidate"),
        "public_id":    public_id
    }
    return call_api("destroy", params, **options)


def rename(from_public_id, to_public_id, **options):
    params = {
        "timestamp": utils.now(),
        "type": options.get("type"),
        "overwrite": options.get("overwrite"),
        "invalidate": options.get("invalidate"),
        "from_public_id": from_public_id,
        "to_public_id": to_public_id
    }
    return call_api("rename", params, **options)


def explicit(public_id, **options):
    params = utils.build_upload_params(**options)
    params["public_id"] = public_id
    return call_api("explicit", params, **options)


def create_archive(**options):
    params = utils.archive_params(**options)
    if options.get("target_format") is not None:
        params["target_format"] = options.get("target_format")
    return call_api("generate_archive", params, **options)


def create_zip(**options):
    return create_archive(target_format="zip", **options)


def generate_sprite(tag, **options):
    params = {
        "timestamp": utils.now(),
        "tag": tag,
        "async": options.get("async"),
        "notification_url": options.get("notification_url"),
        "transformation": utils.generate_transformation_string(fetch_format=options.get("format"), **options)[0]
    }
    return call_api("sprite", params, **options)


def multi(tag, **options):
    params = {
        "timestamp": utils.now(),
        "tag": tag,
        "format": options.get("format"),
        "async": options.get("async"),
        "notification_url": options.get("notification_url"),
        "transformation": utils.generate_transformation_string(**options)[0]
    }
    return call_api("multi", params, **options)


def explode(public_id, **options):
    params = {
        "timestamp": utils.now(),
        "public_id": public_id,
        "format": options.get("format"),
        "notification_url": options.get("notification_url"),
        "transformation": utils.generate_transformation_string(**options)[0]
    }
    return call_api("explode", params, **options)


# options may include 'exclusive' (boolean) which causes clearing this tag from all other resources
def add_tag(tag, public_ids=None, **options):
    exclusive = options.pop("exclusive", None)
    command = "set_exclusive" if exclusive else "add"
    return call_tags_api(tag, command, public_ids, **options)


def remove_tag(tag, public_ids=None, **options):
    return call_tags_api(tag, "remove", public_ids, **options)


def replace_tag(tag, public_ids=None, **options):
    return call_tags_api(tag, "replace", public_ids, **options)


def remove_all_tags(public_ids, **options):
    """
    Remove all tags from the specified public IDs.
    :param public_ids: the public IDs of the resources to update
    :param options: additional options passed to the request
    :return: dictionary with a list of public IDs that were updated
    """
    return call_tags_api(None, "remove_all", public_ids, **options)


def add_context(context, public_ids, **options):
    """
    Add a context keys and values. If a particular key already exists, the value associated with the key is updated.
    :param context: dictionary of context
    :param public_ids: the public IDs of the resources to update
    :param options: additional options passed to the request
    :return: dictionary with a list of public IDs that were updated
    """
    return call_context_api(context, "add", public_ids, **options)


def remove_all_context(public_ids, **options):
    """
    Remove all custom context from the specified public IDs.
    :param public_ids: the public IDs of the resources to update
    :param options: additional options passed to the request
    :return: dictionary with a list of public IDs that were updated
    """
    return call_context_api(None, "remove_all", public_ids, **options)


def call_tags_api(tag, command, public_ids=None, **options):
    params = {
        "timestamp": utils.now(),
        "tag": tag,
        "public_ids": utils.build_array(public_ids),
        "command": command,
        "type": options.get("type")
    }
    return call_api("tags", params, **options)


def call_context_api(context, command, public_ids=None, **options):
    params = {
        "timestamp": utils.now(),
        "context": utils.encode_context(context),
        "public_ids": utils.build_array(public_ids),
        "command": command,
        "type": options.get("type")
    }
    return call_api("context", params, **options)


TEXT_PARAMS = ["public_id",
               "font_family",
               "font_size",
               "font_color",
               "text_align",
               "font_weight",
               "font_style",
               "background",
               "opacity",
               "text_decoration"
               ]


def text(text, **options):
    params = {"timestamp": utils.now(), "text": text}
    for key in TEXT_PARAMS:
        params[key] = options.get(key)
    return call_api("text", params, **options)


def call_api(action, params, http_headers=None, return_error=False, unsigned=False, file=None, timeout=None, **options):
    if http_headers is None:
        http_headers = {}
    file_io = None
    try:
        if unsigned:
            params = utils.cleanup_params(params)
        else:
            params = utils.sign_request(params, options)

        param_list = OrderedDict()
        for k, v in params.items():
            if isinstance(v, list):
                for i in range(len(v)):
                    param_list["{0}[{1}]".format(k, i)] = v[i]
            elif v:
                param_list[k] = v

        api_url = utils.cloudinary_api_url(action, **options)
        if file:
            if isinstance(file, string_types):
                if re.match(r'ftp:|https?:|s3:|data:[^;]*;base64,([a-zA-Z0-9\/+\n=]+)$', file):
                    # URL
                    name = None
                    data = file
                else:
                    # file path
                    name = file
                    with open(file, "rb") as opened:
                        data = opened.read()
            elif hasattr(file, 'read') and callable(file.read):
                # stream
                data = file.read()
                name = file.name if hasattr(file, 'name') and isinstance(file.name, str) else "stream"
            elif isinstance(file, tuple):
                name = None
                data = file
            else:
                # Not a string, not a stream
                name = "file"
                data = file

            param_list["file"] = (name, data) if name else data

        headers = {"User-Agent": cloudinary.get_user_agent()}
        headers.update(http_headers)

        kw = {}
        if timeout is not None:
            kw['timeout'] = timeout

        code = 200
        try:
            response = _http.request("POST", api_url, param_list, headers, **kw)
        except HTTPError as e:
            raise Error("Unexpected error - {0!r}".format(e))
        except socket.error as e:
            raise Error("Socket error: {0!r}".format(e))

        try:
            result = json.loads(response.data.decode('utf-8'))
        except Exception as e:
            # Error is parsing json
            raise Error("Error parsing server response (%d) - %s. Got - %s", response.status, response, e)

        if "error" in result:
            if response.status not in [200, 400, 401, 403, 404, 500]:
                code = response.status
            if return_error:
                    result["error"]["http_code"] = code
            else:
                raise Error(result["error"]["message"])

        return result
    finally:
        if file_io: file_io.close()