<?php
namespace Cloudinary {

    class Error extends \Exception {};  

    class Uploader {
        public static function build_upload_params(&$options)
        {
            $params = array("timestamp" => time(),
                "allowed_formats" => \Cloudinary::encode_array(\Cloudinary::option_get($options, "allowed_formats")),
                "auto_tagging" => \Cloudinary::option_get($options, "auto_tagging"),
                "background_removal" => \Cloudinary::option_get($options, "background_removal"),
                "backup" => \Cloudinary::option_get($options, "backup"),
                "callback" => \Cloudinary::option_get($options, "callback"),
                "categorization" => \Cloudinary::option_get($options, "categorization"),
                "colors" => \Cloudinary::option_get($options, "colors"),
                "context" => \Cloudinary::encode_assoc_array(\Cloudinary::option_get($options, "context")),
                "custom_coordinates" => \Cloudinary::encode_double_array(\Cloudinary::option_get($options, "custom_coordinates")),
                "detection" => \Cloudinary::option_get($options, "detection"),
                "discard_original_filename" => \Cloudinary::option_get($options, "discard_original_filename"),
                "eager" => Uploader::build_eager(\Cloudinary::option_get($options, "eager")),
                "eager_async" => \Cloudinary::option_get($options, "eager_async"),
                "eager_notification_url" => \Cloudinary::option_get($options, "eager_notification_url"),
                "exif" => \Cloudinary::option_get($options, "exif"),
                "face_coordinates" => \Cloudinary::encode_double_array(\Cloudinary::option_get($options, "face_coordinates")),
                "faces" => \Cloudinary::option_get($options, "faces"),
                "folder" => \Cloudinary::option_get($options, "folder"),
                "format" => \Cloudinary::option_get($options, "format"),
                "headers" => Uploader::build_custom_headers(\Cloudinary::option_get($options, "headers")),
                "image_metadata" => \Cloudinary::option_get($options, "image_metadata"),
                "invalidate" => \Cloudinary::option_get($options, "invalidate"),
                "moderation" => \Cloudinary::option_get($options, "moderation"),
                "notification_url" => \Cloudinary::option_get($options, "notification_url"),
                "ocr" => \Cloudinary::option_get($options, "ocr"),
                "overwrite" => \Cloudinary::option_get($options, "overwrite"),
                "phash" => \Cloudinary::option_get($options, "phash"),
                "proxy" => \Cloudinary::option_get($options, "proxy"),
                "public_id" => \Cloudinary::option_get($options, "public_id"),
                "raw_convert" => \Cloudinary::option_get($options, "raw_convert"),
                "return_delete_token" => \Cloudinary::option_get($options, "return_delete_token"),
                "similarity_search" => \Cloudinary::option_get($options, "similarity_search"),
                "tags" => implode(",", \Cloudinary::build_array(\Cloudinary::option_get($options, "tags"))),
                "transformation" => \Cloudinary::generate_transformation_string($options),
                "type" => \Cloudinary::option_get($options, "type"),
                "unique_filename" => \Cloudinary::option_get($options, "unique_filename"),
                "upload_preset" => \Cloudinary::option_get($options, "upload_preset"),
                "use_filename" => \Cloudinary::option_get($options, "use_filename"),
                "responsive_breakpoints" => Uploader::build_responsive_breakpoints(\Cloudinary::option_get($options, "responsive_breakpoints"))
            );
            array_walk($params, function (&$value, $key){ $value = (is_bool($value) ? ($value ? "1" : "0") : $value);});
            return array_filter($params,function($v){ return !is_null($v) && ($v !== "" );});
        }

        public static function unsigned_upload($file, $upload_preset, $options = array())
        {
            return Uploader::upload($file, array_merge($options, array("unsigned"=>TRUE, "upload_preset"=>$upload_preset)));
        }

        public static function upload($file, $options = array())
        {
            $params = Uploader::build_upload_params($options);
            return Uploader::call_api("upload", $params, $options, $file);
        }

        // Upload large raw files. Note that public_id should include an extension for best results.
        public static function upload_large($file, $options=array())
        {
            $src = fopen($file, 'r');
            $temp_file_name = tempnam(sys_get_temp_dir(), 'cldupload.' . pathinfo($file, PATHINFO_EXTENSION));
            $upload = $upload_id = NULL;
            $chunk_size = \Cloudinary::option_get($options, "chunk_size", 20000000);
            $public_id = \Cloudinary::option_get($options, "public_id");
            $index = 0;
            $file_size = filesize($file);
            while (!feof($src)) {
                $current_loc = $index * $chunk_size;
                if ($current_loc >= $file_size) {
                    break;
                }
                $dest = fopen($temp_file_name, 'w');
                stream_copy_to_stream($src, $dest, $chunk_size);
                fclose($dest);
                if (phpversion() >= "5.3.0") {
                    clearstatcache(TRUE, $temp_file_name);
                } else {
                    clearstatcache();
                }

                $temp_file_size = filesize($temp_file_name);
                $range = "bytes ". $current_loc . "-" . ($current_loc + $temp_file_size - 1) . "/" . $file_size;
                try {
                    $upload = Uploader::upload_large_part($temp_file_name, array_merge($options, 
                                array("public_id"=>$public_id, "content_range"=>$range)));
                } catch(\Exception $e) {
                    unlink($temp_file_name);
                    fclose($src);
                    throw $e;                    
                }
                $upload_id = \Cloudinary::option_get($upload, "upload_id");
                $public_id = \Cloudinary::option_get($upload, "public_id");
                $index += 1;
            }
            unlink($temp_file_name);
            fclose($src);
            return $upload;
        }
    

        // Upload large raw files. Note that public_id should include an extension for best results.
        public static function upload_large_part($file, $options=array())
        {
            $params = Uploader::build_upload_params($options);
            return Uploader::call_api("upload_chunked", $params, array_merge(array("resource_type" => "raw"), $options), $file);
        }

        public static function destroy($public_id, $options = array())
        {
            $params = array(
                "timestamp" => time(),
                "type" => \Cloudinary::option_get($options, "type"),
                "invalidate" => \Cloudinary::option_get($options, "invalidate"),
                "public_id" => $public_id
            );
            return Uploader::call_api("destroy", $params, $options);
        }

        public static function rename($from_public_id, $to_public_id, $options = array())
        {
            $params = array(
                "timestamp" => time(),
                "type" => \Cloudinary::option_get($options, "type"),
                "from_public_id" => $from_public_id,
                "to_public_id" => $to_public_id,
                "invalidate" => \Cloudinary::option_get($options, "invalidate"),
                "overwrite" => \Cloudinary::option_get($options, "overwrite"),
                "to_type" => \Cloudinary::option_get($options, "to_type"),
            );
            return Uploader::call_api("rename", $params, $options);
        }
        
        public static function explicit($public_id, $options = array())
        {
            $options["public_id"] = $public_id;
            $params = Uploader::build_upload_params($options);
            return Uploader::call_api("explicit", $params, $options);
        }

        public static function generate_sprite($tag, $options = array())
        {
            $transformation = \Cloudinary::generate_transformation_string(
              array_merge(array("fetch_format"=>\Cloudinary::option_get($options, "format")), $options));
            $params = array(
                "timestamp" => time(),
                "tag" => $tag,
                "async" => \Cloudinary::option_get($options, "async"),
                "notification_url" => \Cloudinary::option_get($options, "notification_url"),
                "transformation" => $transformation
            );
            return Uploader::call_api("sprite", $params, $options);
        }

        public static function multi($tag, $options = array())
        {
            $transformation = \Cloudinary::generate_transformation_string($options);
            $params = array(
                "timestamp" => time(),
                "tag" => $tag,
                "format" => \Cloudinary::option_get($options, "format"),
                "async" => \Cloudinary::option_get($options, "async"),
                "notification_url" => \Cloudinary::option_get($options, "notification_url"),
                "transformation" => $transformation
            );
            return Uploader::call_api("multi", $params, $options);
        }

        public static function explode($public_id, $options = array())
        {
            $transformation = \Cloudinary::generate_transformation_string($options);
            $params = array(
                "timestamp" => time(),
                "public_id" => $public_id,
                "format" => \Cloudinary::option_get($options, "format"),
                "type" => \Cloudinary::option_get($options, "type"),
                "notification_url" => \Cloudinary::option_get($options, "notification_url"),
                "transformation" => $transformation
            );
            return Uploader::call_api("explode", $params, $options);
        }

        public static function add_tag($tag, $public_ids = array(), $options = array())
        {
            return Uploader::call_tags_api($tag, "add", $public_ids, $options);
        }

        public static function remove_tag($tag, $public_ids = array(), $options = array())
        {
            return Uploader::call_tags_api($tag, "remove", $public_ids, $options);
        }

        public static function replace_tag($tag, $public_ids = array(), $options = array())
        {
            return Uploader::call_tags_api($tag, "replace", $public_ids, $options);
        }

        public static function call_tags_api($tag, $command, $public_ids = array(), &$options = array())
        {
            $params = array(
                "timestamp" => time(),
                "tag" => $tag,
                "public_ids" => \Cloudinary::build_array($public_ids),
                "type" => \Cloudinary::option_get($options, "type"),
                "command" => $command
            );
            return Uploader::call_api("tags", $params, $options);
        }

        public static function add_context($context, $public_ids = array(), $options = array()) {
          return Uploader::call_context_api($context, 'add', $public_ids, $options);
        }

        public static function remove_all_context($public_ids = array(), $options = array()) {
          return Uploader::call_context_api(null, 'remove_all', $public_ids, $options);
        }

        public static function call_context_api($context, $command, $public_ids = array(), &$options = array())
        {
            $params = array(
                "timestamp" => time(),
                "context" => $context,
                "public_ids" => \Cloudinary::build_array($public_ids),
                "type" => \Cloudinary::option_get($options, "type"),
                "command" => $command
            );
            return Uploader::call_api("context", $params, $options);
        }

        private static $TEXT_PARAMS = array("public_id", "font_family", "font_size", "font_color", "text_align", "font_weight", "font_style", "background", "opacity", "text_decoration");

        public static function text($text, $options = array())
        {
            $params = array("timestamp" => time(), "text" => $text);
            foreach (Uploader::$TEXT_PARAMS as $key) {
                $params[$key] = \Cloudinary::option_get($options, $key);
            }
            return Uploader::call_api("text", $params, $options);
        }

        # Creates a new archive in the server and returns information in JSON format
        public static function create_archive($options = array(), $target_format = NULL)
        {
            $params = \Cloudinary::build_archive_params($options);
            if ($target_format != NULL) {
                $params["target_format"] = $target_format;
            }
            return Uploader::call_api("generate_archive", $params, $options);
        }

        # Creates a new zip archive in the server and returns information in JSON format
        public static function create_zip($options = array())
        {
            return Uploader::create_archive($options, "zip");
        }

        public static function call_api($action, $params, $options = array(), $file = NULL)
        {
            $return_error = \Cloudinary::option_get($options, "return_error");
            if (!\Cloudinary::option_get($options, "unsigned")) {
                $params = \Cloudinary::sign_request($params, $options);
            }

            $api_url = \Cloudinary::cloudinary_api_url($action, $options);

            $ch = curl_init($api_url);

            $post_params = array();
            foreach ($params as $key => $value) {
                if (is_array($value)) {
                    $i = 0;
                    foreach ($value as $item) {
                        $post_params[$key . "[$i]"] = $item;
                        $i++;
                    }
                } else {
                    $post_params[$key] = $value;
                }
            }
            if ($file) {
                if (!preg_match('/^@|^ftp:|^https?:|^s3:|^data:[^;]*;base64,([a-zA-Z0-9\/+\n=]+)$/', $file)) {
                    if (function_exists("curl_file_create")) {
                        $post_params['file'] = curl_file_create($file);
                        $post_params['file']->setPostFilename($file);
                    } else {
                        $post_params["file"] = "@" . $file;
                    }
                } else {
                    $post_params["file"] = $file;
                }
            }

            curl_setopt($ch, CURLOPT_POST, true);
            $timeout = \Cloudinary::option_get($options, "timeout", \Cloudinary::config_get("timeout", 60));
            curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
            $connection_timeout = \Cloudinary::option_get($options, "connection_timeout", \Cloudinary::config_get("connection_timeout"));
            if ($connection_timeout != NULL) {
                curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $connection_timeout);
            }
            curl_setopt($ch, CURLOPT_POSTFIELDS, $post_params);
            curl_setopt($ch, CURLOPT_CAINFO,realpath(dirname(__FILE__)).DIRECTORY_SEPARATOR."cacert.pem");
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); # no effect since PHP 5.1.3
            curl_setopt($ch, CURLOPT_USERAGENT, \Cloudinary::userAgent());
            curl_setopt($ch, CURLOPT_PROXY, \Cloudinary::option_get($options, "api_proxy", \Cloudinary::config_get("api_proxy")));

            $range = \Cloudinary::option_get($options, "content_range");
            if ($range != NULL){
                curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Range: '.$range));
            }
            
            $response = curl_exec($ch);
            $curl_error = NULL;
            if(curl_errno($ch))
            {
                $curl_error = curl_error($ch);
            }

            $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $response_data = $response;

            curl_close($ch);
            if ($curl_error != NULL) {
                throw new \Cloudinary\Error("Error in sending request to server - " . $curl_error);
            }
            if ($code != 200 && $code != 400 && $code != 500 && $code != 401 && $code != 404) {
                throw new \Cloudinary\Error("Server returned unexpected status code - " . $code . " - " . $response_data, $code);
            }
            $result = json_decode($response_data, TRUE);
            if ($result == NULL) {
                throw new \Cloudinary\Error("Error parsing server response (" . $code . ") - " . $response_data);
            }
            if (isset($result["error"])) {
                if ($return_error) {
                    $result["error"]["http_code"] = $code;
                } else {
                    throw new \Cloudinary\Error($result["error"]["message"], $code);
                }
            }
            return $result;
        }

        protected static function build_eager($transformations) {
            return \Cloudinary::build_eager($transformations);
        }

        protected static function build_responsive_breakpoints($breakpoints) {
            if (!$breakpoints) {
                return NULL;
            }
            $breakpoints_params = array();
            foreach (\Cloudinary::build_array($breakpoints) as $breakpoint_settings) {
                if ($breakpoint_settings) {
                    $transformation = \Cloudinary::option_consume($breakpoint_settings, "transformation");
                    if ($transformation) {
                        $breakpoint_settings["transformation"] = \Cloudinary::generate_transformation_string($transformation);
                    }
                    array_push($breakpoints_params, $breakpoint_settings);
                }
            }
            return json_encode($breakpoints_params);
        }

        protected static function build_custom_headers($headers) {
            if ($headers == NULL) {
                return NULL;
            } elseif (is_string($headers)) {
                return $headers;
            } elseif ($headers == array_values($headers)) {
                return implode("\n", $headers);
            } else {
                $join_pair = function($key, $value) { return $key . ": " . $value; };
                return implode("\n", array_map($join_pair, array_keys($headers), array_values($headers)));
            }
        }  
    }
}
