/*
 * Copyright 2017 Baidu, Inc.
 *
 * 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.
 */

// object.go - the object APIs definition supported by the BOS service

package api

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"hash"
	"io"
	"io/ioutil"
	"mime/multipart"
	"net"
	"strconv"
	"strings"
	"time"

	"github.com/baidubce/bce-sdk-go/auth"
	"github.com/baidubce/bce-sdk-go/bce"
	"github.com/baidubce/bce-sdk-go/http"
	"github.com/baidubce/bce-sdk-go/util"
	"github.com/baidubce/bce-sdk-go/util/log"
)

// PutObject - put the object from the string or the stream
//
// PARAMS:
//   - cli: the client agent which can perform sending request
//   - bucket: the bucket name of the object
//   - object: the name of the object
//   - body: the input content of the object
//   - args: the optional arguments of this api
//
// RETURNS:
//   - string: the etag of the object
//   - error: nil if ok otherwise the specific error
func PutObject(cli bce.Client, bucket, object string, body *bce.Body, args *PutObjectArgs,
	ctx *BosContext, options ...Option) (string, *PutObjectResult, error) {
	req := &BosRequest{}
	resp := &BosResponse{}
	NeedReturnCallback := false
	req.SetUri(getObjectUri(bucket, object))
	req.SetMethod(http.PUT)
	req.SetBucket(bucket)
	if body == nil {
		return "", nil, bce.NewBceClientError("PutObject body should not be emtpy")
	}
	if ctx == nil {
		ctx = newDefaultBosContext()
	}
	if body.Size() >= THRESHOLD_100_CONTINUE {
		req.SetHeader("Expect", "100-continue")
	}
	req.SetBody(body)
	// Optional arguments settings
	if args != nil {
		setOptionalNullHeaders(req, map[string]string{
			http.CACHE_CONTROL:                      args.CacheControl,
			http.CONTENT_DISPOSITION:                args.ContentDisposition,
			http.CONTENT_TYPE:                       args.ContentType,
			http.EXPIRES:                            args.Expires,
			http.BCE_CONTENT_SHA256:                 args.ContentSha256,
			http.BCE_CONTENT_CRC32:                  args.ContentCrc32,
			http.BCE_CONTENT_CRC32C:                 args.ContentCrc32c,
			http.BCE_CONTENT_CRC32C_FLAG:            strconv.FormatBool(args.ContentCrc32cFlag),
			http.CONTENT_ENCODING:                   args.ContentEncoding,
			http.BCE_SERVER_SIDE_ENCRYPTION:         args.Encryption.ServerSideEncryption,
			http.BCE_SERVER_SIDE_ENCRYPTION_KEY:     args.Encryption.SSECKey,
			http.BCE_SERVER_SIDE_ENCRYPTION_KEY_ID:  args.Encryption.SSEKmsKeyId,
			http.BCE_SERVER_SIDE_ENCRYPTION_KEY_MD5: args.Encryption.SSECKeyMD5,
			http.BCE_FORBID_OVERWRITE:               strconv.FormatBool(args.ForbidOverwrite),
			http.BCE_CONTENT_CRC64ECMA:              args.ContentCrc64ECMA,
		})

		if args.ContentLength > body.Size() {
			return "", nil, bce.NewBceClientError(fmt.Sprintf("ContentLength %d is bigger than body size %d", args.ContentLength, body.Size()))
		}
		if args.ContentLength > 0 && args.ContentLength < body.Size() {
			// User specified Content-Length can be smaller than the body size, so the body should
			// be reset. The `net/http.Client' does not support the Content-Length bigger than the
			// body size.
			newBody, err := bce.NewBodyFromSizedReaderV2(body.Stream(), args.ContentLength, false)
			if err != nil {
				return "", nil, bce.NewBceClientError(err.Error())
			}
			req.SetHeader(http.CONTENT_LENGTH, fmt.Sprintf("%d", args.ContentLength))
			req.SetBody(newBody) // re-assign body
		}

		if args.TrafficLimit > 0 && (args.TrafficLimit > TRAFFIC_LIMIT_MAX || args.TrafficLimit < TRAFFIC_LIMIT_MIN) {
			return "", nil, fmt.Errorf("TrafficLimit must between %d ~ %d, current value:%d",
				TRAFFIC_LIMIT_MIN, TRAFFIC_LIMIT_MAX, args.TrafficLimit)
		}

		if len(args.StorageClass) != 0 && !validStorageClass(args.StorageClass) {
			return "", nil, fmt.Errorf("invalid storage class value: %s", args.StorageClass)
		}

		if err := setUserMetadata(req, args.UserMeta); err != nil {
			return "", nil, err
		}

		if strings.HasPrefix(args.Process, "callback") {
			NeedReturnCallback = true
		}

		options = append(options, ObjectExpires(args.ObjectExpires))
		options = append(options, ContentMD5(args.ContentMD5))
		options = append(options, CannedAcl(args.CannedAcl))
		options = append(options, Process(args.Process))
		options = append(options, TaggingStr(args.ObjectTagging))
		options = append(options, StorageClass(args.StorageClass))
		options = append(options, TrafficLimit(args.TrafficLimit))
		options = append(options, IfMatch(args.IfMatch))
		options = append(options, IfNoneMatch(args.IfNoneMatch))
	}
	// add content-type if not assigned by user
	if req.Header(http.CONTENT_TYPE) == "" {
		req.SetHeader(http.CONTENT_TYPE, getDefaultContentType(object))
	}
	// handle options to set the header/params of request
	if err := handleOptions(req, options); err != nil {
		return "", nil, bce.NewBceClientError(fmt.Sprintf("Handle options occur error: %s", err))
	}
	AddCrc64Check(req, resp)
	for _, tracker := range req.Tracker {
		if err := tracker(req); err != nil {
			return "", nil, fmt.Errorf("handle request tracker failed: %s", err)
		}
	}

	if err := SendRequest(cli, req, resp, ctx); err != nil {
		return "", nil, err
	}
	if resp.IsFail() {
		return "", nil, resp.ServiceError()
	}
	defer func() { resp.Body().Close() }()

	//get header
	jsonBody := &PutObjectResult{}
	getOptions := []GetOption{
		getHeader(http.BCE_VERSION_ID, &jsonBody.VersionId),
		getHeader(http.BCE_STORAGE_CLASS, &jsonBody.StorageClass),
		getHeader(http.BCE_CONTENT_CRC32, &jsonBody.ContentCrc32),
		getHeader(http.BCE_CONTENT_CRC32C, &jsonBody.ContentCrc32c),
		getHeader(http.BCE_SERVER_SIDE_ENCRYPTION, &jsonBody.ServerSideEncryption),
		getHeader(http.BCE_CONTENT_CRC64ECMA, &jsonBody.ContentCrc64ECMA),
	}
	if err := handleGetOptions(resp, getOptions); err != nil {
		return "", nil, bce.NewBceClientError(fmt.Sprintf("Handle get options error: %s", err))
	}
	if NeedReturnCallback {
		if err := resp.ParseJsonBody(jsonBody); err != nil {
			return "", nil, err
		}
		return strings.Trim(resp.Header(http.ETAG), "\""), jsonBody, nil
	}
	return strings.Trim(resp.Header(http.ETAG), "\""), jsonBody, nil
}

// OptionsObject - Get the options of the given object for CORS
//
// PARAMS:
//   - cli: the client agent which can perform sending request
//   - bucket: the bucket name of the object
//   - object: the name of the object
//   - args: the optional arguments of this api
//
// RETURNS:
//   - result: the supported options of the given object
//   - error: nil if ok otherwise the specific error
func OptionsObject(cli bce.Client, bucket, object string, args *OptionsObjectArgs,
	ctx *BosContext, options ...Option) (*OptionsObjectResult, error) {
	req := &BosRequest{}
	req.SetMethod(http.OPTIONS)
	req.SetUri(getObjectUri(bucket, object))
	req.SetBucket(bucket)
	if ctx == nil {
		ctx = newDefaultBosContext()
	}
	options = append(options, setHeader(http.ORIGIN, args.Origin))
	options = append(options, setHeader(http.ACCESS_CONTROL_REQUEST_METHOD, args.RequestMethod))
	options = append(options, setHeader(http.ACCESS_CONTROL_REQUEST_HEADERS, strings.Join(args.RequestHeaders, ",")))
	// handle options to set the header/params of request
	if err := handleOptions(req, options); err != nil {
		return nil, bce.NewBceClientError(fmt.Sprintf("Handle options error: %s", err))
	}
	resp := &BosResponse{}
	if err := SendRequest(cli, req, resp, ctx); err != nil {
		return nil, err
	}
	if resp.IsFail() {
		return nil, resp.ServiceError()
	}
	defer func() { resp.Body().Close() }()

	//get header
	result := &OptionsObjectResult{}
	getOptions := []GetOption{
		getHeader(http.ACCESS_CONTROL_ALLOW_CREDENTIALS, &result.AllowCredentials),
		getHeader(http.ACCESS_CONTROL_ALLOW_HEADERS, &result.AllowHeaders),
		getHeader(http.ACCESS_CONTROL_ALLOW_METHODS, &result.AllowMethods),
		getHeader(http.ACCESS_CONTROL_ALLOW_ORIGIN, &result.AllowOrigin),
		getHeader(http.ACCESS_CONTROL_EXPOSE_HEADERS, &result.ExposeHeaders),
		getHeader(http.ACCESS_CONTROL_MAX_AGE, &result.MaxAge),
	}
	if err := handleGetOptions(resp, getOptions); err != nil {
		return nil, bce.NewBceClientError(fmt.Sprintf("Handle get options error: %s", err))
	}
	return result, nil
}

// PostObject - put the object by multipart/form-data
//
// PARAMS:
//   - cli: the client agent which can perform sending request
//   - bucket: the bucket name of the object
//   - object: the name of the object
//   - body: the input content of the object
//   - args: the optional arguments of this api
//
// RETURNS:
//   - result: the result of post object
//   - error: nil if ok otherwise the specific error
func PostObject(cli bce.Client, bucket, object string, content *bytes.Buffer, args *PostObjectArgs,
	ctx *BosContext, options ...Option) (*PostObjectResult, error) {
	if args == nil {
		return nil, bce.NewBceClientError("post object argument is nil.")
	}
	req := &BosRequest{}
	req.SetMethod(http.POST)
	req.SetUri(getBucketUri(bucket))
	req.SetBucket(bucket)
	if ctx == nil {
		ctx = newDefaultBosContext()
	}

	//post policy
	expiration := time.Now().UTC().Add(args.Expiration)
	policyMap := map[string]interface{}{
		"expiration": expiration.Format(util.ISO8601Format),
		"conditions": []interface{}{
			map[string]string{"bucket": bucket},
			map[string]string{"key": object},
			[]interface{}{
				"content-length-range",
				args.ContentLengthLower,
				args.ContentLengthUpper,
			},
		},
	}
	//json serialize policyMap
	policy, err := json.Marshal(policyMap)
	if err != nil {
		return nil, err
	}
	// calc post signature
	cred := cli.GetBceClientConfig().Credentials
	stringToSign := base64.StdEncoding.EncodeToString([]byte(policy))
	hmacHash := func() hash.Hash { return sha256.New() }
	h := hmac.New(hmacHash, []byte(cred.SecretAccessKey))
	_, err = io.WriteString(h, stringToSign)
	if err != nil {
		return nil, err
	}
	signature := hex.EncodeToString(h.Sum(nil))
	// build post body
	bodyBuf := &bytes.Buffer{}
	bodyWriter := multipart.NewWriter(bodyBuf)
	options = append(options, SetPostField("accessKey", cred.AccessKeyId))
	options = append(options, SetPostField("policy", stringToSign))
	options = append(options, SetPostField("signature", signature))
	options = append(options, SetPostField("key", object))
	if err := handlePostOptions(bodyWriter, options); err != nil {
		return nil, bce.NewBceClientError(fmt.Sprintf("Handle post options error: %s", err))
	}
	// create a field named 'file', used to upload content
	w, _ := bodyWriter.CreateFormField("file")
	_, err = io.Copy(w, bytes.NewReader(content.Bytes()))
	if err != nil {
		return nil, err
	}
	bodyWriter.Close()
	//build bcebody
	body := &bce.Body{}
	body.SetStream(ioutil.NopCloser(bytes.NewBuffer(bodyBuf.Bytes())))
	body.SetSize(int64(len(bodyBuf.Bytes())))
	contentMD5, err := util.CalculateContentMD5(content, int64(content.Len()))
	if err != nil {
		return nil, err
	}
	body.SetContentMD5(contentMD5)
	if body.Size() >= THRESHOLD_100_CONTINUE {
		req.SetHeader("Expect", "100-continue")
	}
	req.SetBody(body)
	req.SetHeader(http.CONTENT_TYPE, bodyWriter.FormDataContentType())
	// handle options to set the header/params of request
	if err := handleOptions(req, options); err != nil {
		return nil, bce.NewBceClientError(fmt.Sprintf("Handle options error: %s", err))
	}

	resp := &BosResponse{}
	if err := SendRequest(cli, req, resp, ctx); err != nil {
		return nil, err
	}
	if resp.IsFail() {
		return nil, resp.ServiceError()
	}
	defer func() { resp.Body().Close() }()

	//get header
	result := &PostObjectResult{}
	getOptions := []GetOption{
		getHeader(http.ETAG, &result.ETag),
		getHeader(http.CONTENT_MD5, &result.ContentMD5),
		getHeader(http.BCE_CONTENT_CRC32, &result.ContentCrc32),
	}
	if err := handleGetOptions(resp, getOptions); err != nil {
		return nil, bce.NewBceClientError(fmt.Sprintf("Handle get options error: %s", err))
	}
	result.ETag = strings.Trim(result.ETag, "\"")
	return result, nil
}

// CopyObject - copy one object to a new object with new bucket and/or name. It can alse set the
// metadata of the object with the same source and target.
//
// PARAMS:
//   - cli: the client object which can perform sending request
//   - bucket: the bucket name of the target object
//   - object: the name of the target object
//   - source: the source object uri
//   - *CopyObjectArgs: the optional input args for copying object
//
// RETURNS:
//   - *CopyObjectResult: the result object which contains etag and lastmodified
//   - error: nil if ok otherwise the specific error
func CopyObject(cli bce.Client, bucket, object, source string, args *CopyObjectArgs,
	ctx *BosContext, options ...Option) (*CopyObjectResult, error) {
	req := &BosRequest{}
	req.SetUri(getObjectUri(bucket, object))
	req.SetMethod(http.PUT)
	req.SetBucket(bucket)
	if len(source) == 0 {
		return nil, bce.NewBceClientError("copy source should not be null")
	}
	if len(args.SrcVersionId) > 0 {
		source = source + "?versionId=" + args.SrcVersionId
	}
	req.SetHeader(http.BCE_COPY_SOURCE, util.UriEncode(source, false))
	if ctx == nil {
		ctx = newDefaultBosContext()
	}
	// Optional arguments settings
	if args != nil {
		setOptionalNullHeaders(req, map[string]string{
			http.CACHE_CONTROL:                       args.CacheControl,
			http.CONTENT_DISPOSITION:                 args.ContentDisposition,
			http.CONTENT_ENCODING:                    args.ContentEncoding,
			http.CONTENT_RANGE:                       args.ContentRange,
			http.CONTENT_TYPE:                        args.ContentType,
			http.EXPIRES:                             args.Expires,
			http.LAST_MODIFIED:                       args.LastModified,
			http.ETAG:                                args.ETag,
			http.CONTENT_MD5:                         args.ContentMD5,
			http.BCE_CONTENT_SHA256:                  args.ContentSha256,
			http.BCE_OBJECT_TYPE:                     args.ObjectType,
			http.BCE_NEXT_APPEND_OFFSET:              args.NextAppendOffset,
			http.BCE_COPY_SOURCE_IF_MATCH:            args.IfMatch,
			http.BCE_COPY_SOURCE_IF_NONE_MATCH:       args.IfNoneMatch,
			http.BCE_COPY_SOURCE_IF_MODIFIED_SINCE:   args.IfModifiedSince,
			http.BCE_COPY_SOURCE_IF_UNMODIFIED_SINCE: args.IfUnmodifiedSince,
			http.BCE_CONTENT_CRC32C:                  args.ContentCrc32c,
			http.BCE_CONTENT_CRC32C_FLAG:             strconv.FormatBool(args.ContentCrc32cFlag),
			http.BCE_SERVER_SIDE_ENCRYPTION:          args.Encryption.ServerSideEncryption,
			http.BCE_SERVER_SIDE_ENCRYPTION_KEY:      args.Encryption.SSECKey,
			http.BCE_SERVER_SIDE_ENCRYPTION_KEY_ID:   args.Encryption.SSEKmsKeyId,
			http.BCE_SERVER_SIDE_ENCRYPTION_KEY_MD5:  args.Encryption.SSECKeyMD5,
			http.BCE_CONTENT_CRC64ECMA:               args.ContentCrc64ECMA,
		})
		if args.ObjectExpires > 0 {
			req.SetHeader(http.BCE_OBJECT_EXPIRES, fmt.Sprintf("%d", args.ObjectExpires))
		}
		if validMetadataDirective(args.MetadataDirective) {
			req.SetHeader(http.BCE_COPY_METADATA_DIRECTIVE, args.MetadataDirective)
		} else {
			if len(args.MetadataDirective) != 0 {
				return nil, bce.NewBceClientError(
					"invalid metadata directive value: " + args.MetadataDirective)
			}
		}
		if validMetadataDirective(args.TaggingDirective) {
			req.SetHeader(http.BCE_COPY_TAGGING_DIRECTIVE, args.TaggingDirective)
		}
		if len(args.ObjectTagging) != 0 {
			if ok, encodeTagging := validObjectTagging(args.ObjectTagging); ok {
				req.SetHeader(http.BCE_OBJECT_TAGGING, encodeTagging)
			}
		}
		if validStorageClass(args.StorageClass) {
			req.SetHeader(http.BCE_STORAGE_CLASS, args.StorageClass)
		} else {
			if len(args.StorageClass) != 0 {
				return nil, bce.NewBceClientError("invalid storage class value: " +
					args.StorageClass)
			}
		}

		//set traffic-limit
		if args.TrafficLimit > 0 {
			if args.TrafficLimit > TRAFFIC_LIMIT_MAX || args.TrafficLimit < TRAFFIC_LIMIT_MIN {
				return nil, bce.NewBceClientError(fmt.Sprintf("TrafficLimit must between %d ~ %d, current value:%d", TRAFFIC_LIMIT_MIN, TRAFFIC_LIMIT_MAX, args.TrafficLimit))
			}
			req.SetHeader(http.BCE_TRAFFIC_LIMIT, fmt.Sprintf("%d", args.TrafficLimit))
		}

		if validCannedAcl(args.CannedAcl) {
			req.SetHeader(http.BCE_ACL, args.CannedAcl)
		}

		if err := setUserMetadata(req, args.UserMeta); err != nil {
			return nil, err
		}
	}
	// handle options to set the header/params of request
	if err := handleOptions(req, options); err != nil {
		return nil, bce.NewBceClientError(fmt.Sprintf("Handle options occur error: %s", err))
	}

	// Send request and get the result
	resp := &BosResponse{}
	if err := SendRequest(cli, req, resp, ctx); err != nil {
		return nil, err
	}
	if resp.IsFail() {
		return nil, resp.ServiceError()
	}
	jsonBody := &CopyObjectResult{}
	if err := resp.ParseJsonBody(jsonBody); err != nil {
		return nil, err
	}
	if len(jsonBody.Code) > 0 {
		return nil, bce.NewBceServiceError(jsonBody.Code, jsonBody.Message, jsonBody.RequestId, 500)
	}
	if resp.Header(http.BCE_VERSION_ID) != "" {
		jsonBody.VersionId = resp.Header(http.BCE_VERSION_ID)
	}
	if resp.Header(http.BCE_REQUEST_ID) != "" {
		jsonBody.RequestId = resp.Header(http.BCE_REQUEST_ID)
	}
	return jsonBody, nil
}

// GetObject - get the object content with range and response-headers-specified support
//
// PARAMS:
//   - cli: the client agent which can perform sending request
//   - bucket: the bucket name of the object
//   - object: the name of the object
//   - args: the optional args in querysring
//   - ranges: the optional range start and end to get the given object
//
// RETURNS:
//   - *GetObjectResult: the output content result of the object
//   - error: nil if ok otherwise the specific error
func GetObject(cli bce.Client, bucket, object string, ctx *BosContext, args map[string]string, // nolint:gocyclo
	ranges ...int64) (*GetObjectResult, error) {

	if object == "" {
		err := fmt.Errorf("get object don't accept \"\" as a parameter")
		return nil, err
	}
	if ctx == nil {
		ctx = newDefaultBosContext()
	}
	req := &BosRequest{}
	req.SetUri(getObjectUri(bucket, object))
	req.SetMethod(http.GET)
	req.SetBucket(bucket)
	// Optional arguments settings
	for k, v := range args {
		if _, ok := GET_OBJECT_ALLOWED_RESPONSE_HEADERS[k]; ok {
			req.SetParam("response"+k, v)
		}
		if strings.HasPrefix(k, http.BCE_PREFIX) {
			req.SetParam(k, v)
		}
	}
	if len(ranges) != 0 {
		rangeStr := "bytes="
		if len(ranges) == 1 {
			rangeStr += fmt.Sprintf("%d", ranges[0]) + "-"
		} else {
			rangeStr += fmt.Sprintf("%d", ranges[0]) + "-" + fmt.Sprintf("%d", ranges[1])
		}
		req.SetHeader("Range", rangeStr)
	}

	// Send request and get the result
	resp := &BosResponse{}
	if err := SendRequest(cli, req, resp, ctx); err != nil {
		return nil, err
	}
	if resp.IsFail() {
		return nil, resp.ServiceError()
	}

	result := &GetObjectResult{}
	getOptions := getObjectMetaOptions(&result.ObjectMeta)
	if err := handleGetOptions(resp, getOptions); err != nil {
		log.Warnf("Handle get options error: %s", err)
	}

	result.Body = resp.Body()
	return result, nil
}

func GetObjectWithArgs(cli bce.Client, bucket, object string, ctx *BosContext, args *GetObjectArgs,
	options ...Option) (*GetObjectResult, error) {
	if object == "" {
		err := fmt.Errorf("get object don't accept \"\" as a parameter")
		return nil, err
	}
	if ctx == nil {
		ctx = newDefaultBosContext()
	}
	req := &BosRequest{}
	req.SetUri(getObjectUri(bucket, object))
	req.SetMethod(http.GET)
	req.SetBucket(bucket)
	// Optional arguments settings
	if args != nil {
		for k, v := range args.Params {
			if _, ok := GET_OBJECT_ALLOWED_RESPONSE_HEADERS[k]; ok {
				req.SetParam("response"+k, v)
			}
			if strings.HasPrefix(k, http.BCE_PREFIX) {
				req.SetParam(k, v)
			}
		}
		if len(args.Ranges) != 0 {
			rangeStr := "bytes="
			if len(args.Ranges) == 1 {
				rangeStr += fmt.Sprintf("%d", args.Ranges[0]) + "-"
			} else {
				rangeStr += fmt.Sprintf("%d", args.Ranges[0]) + "-" + fmt.Sprintf("%d", args.Ranges[1])
			}
			req.SetHeader("Range", rangeStr)
		}
		options = append(options, IfMatch(args.IfMatch))
		options = append(options, IfNoneMatch(args.IfNoneMatch))
		options = append(options, IfModifiedSince(args.IfModifiedSince))
		options = append(options, IfUnModifiedSince(args.IfUnModifiedSince))
	}
	// handle options to set the header/params of request
	if err := handleOptions(req, options); err != nil {
		return nil, bce.NewBceClientError(fmt.Sprintf("Handle options occur error: %s", err))
	}

	// Send request and get the result
	resp := &BosResponse{}
	if err := SendRequest(cli, req, resp, ctx); err != nil {
		return nil, err
	}
	if resp.IsFail() {
		return nil, resp.ServiceError()
	}

	result := &GetObjectResult{}
	getOptions := getObjectMetaOptions(&result.ObjectMeta)
	if err := handleGetOptions(resp, getOptions); err != nil {
		log.Warnf("Handle get options error: %s", err)
	}

	result.Body = resp.Body()
	return result, nil
}

// GetObjectMeta - get the meta data of the given object
//
// PARAMS:
//   - cli: the client agent which can perform sending request
//   - bucket: the bucket name of the object
//   - object: the name of the object
//
// RETURNS:
//   - *GetObjectMetaResult: the result of this api
//   - error: nil if ok otherwise the specific error
func GetObjectMeta(cli bce.Client, bucket, object string, ctx *BosContext, options ...Option) (*GetObjectMetaResult, error) {
	req := &BosRequest{}
	req.SetUri(getObjectUri(bucket, object))
	req.SetMethod(http.HEAD)
	req.SetBucket(bucket)
	if ctx == nil {
		ctx = newDefaultBosContext()
	}
	// handle options to set the header/params of request
	if err := handleOptions(req, options); err != nil {
		return nil, bce.NewBceClientError(fmt.Sprintf("Handle options occur error: %s", err))
	}
	// Send request and get the result
	resp := &BosResponse{}
	if err := SendRequest(cli, req, resp, ctx); err != nil {
		return nil, err
	}
	if resp.IsFail() {
		return nil, resp.ServiceError()
	}
	result := &GetObjectMetaResult{}
	getOptions := getObjectMetaOptions(&result.ObjectMeta)
	if err := handleGetOptions(resp, getOptions); err != nil {
		log.Warnf("Handle get options error: %s", err)
	}

	defer func() { resp.Body().Close() }()
	return result, nil
}

// SelectObject - select the object content
//
// PARAMS:
//   - cli: the client agent which can perform sending request
//   - bucket: the bucket name of the object
//   - object: the name of the object
//   - args: the optional arguments to perform the select operation
//
// RETURNS:
//   - *SelectObjectResult: the output select content result of the object
//   - error: nil if ok otherwise the specific error
func SelectObject(cli bce.Client, bucket, object string, args *SelectObjectArgs,
	ctx *BosContext, options ...Option) (*SelectObjectResult, error) {
	req := &BosRequest{}
	req.SetUri(getObjectUri(bucket, object))
	req.SetMethod(http.POST)
	req.SetParam("select", "")
	req.SetParam("type", args.SelectType)
	req.SetBucket(bucket)
	if ctx == nil {
		ctx = newDefaultBosContext()
	}
	jsonBytes, jsonErr := json.Marshal(args)
	if jsonErr != nil {
		return nil, jsonErr
	}
	body, err := bce.NewBodyFromBytes(jsonBytes)
	if err != nil {
		return nil, err
	}
	req.SetBody(body)
	// handle options to set the header/params of request
	if err := handleOptions(req, options); err != nil {
		return nil, bce.NewBceClientError(fmt.Sprintf("Handle options occur error: %s", err))
	}
	// Send request and get the result
	resp := &BosResponse{}
	if err := SendRequest(cli, req, resp, ctx); err != nil {
		return nil, err
	}
	if resp.IsFail() {
		return nil, resp.ServiceError()
	}

	result := &SelectObjectResult{}

	result.Body = resp.Body()
	return result, nil
}

// FetchObject - fetch the object by the given url and store it to a bucket
//
// PARAMS:
//   - cli: the client agent which can perform sending request
//   - bucket: the bucket name to store the object
//   - object: the name of the object to be stored
//   - source: the source url to fetch
//   - args: the optional arguments to perform the fetch operation
//
// RETURNS:
//   - *FetchObjectArgs: the result of this api
//   - error: nil if ok otherwise the specific error
func FetchObject(cli bce.Client, bucket, object, source string, args *FetchObjectArgs,
	ctx *BosContext, options ...Option) (*FetchObjectResult, error) {
	req := &BosRequest{}
	req.SetUri(getObjectUri(bucket, object))
	req.SetMethod(http.POST)
	req.SetParam("fetch", "")
	req.SetBucket(bucket)
	if len(source) == 0 {
		return nil, bce.NewBceClientError("invalid fetch source value: " + source)
	}
	if ctx == nil {
		ctx = newDefaultBosContext()
	}
	req.SetHeader(http.BCE_PREFIX+"fetch-source", source)

	// Optional arguments settings
	if args != nil {
		setOptionalNullHeaders(req, map[string]string{
			http.CONTENT_ENCODING: args.ContentEncoding,
		})
		if args.ObjectExpires > 0 {
			req.SetHeader(http.BCE_OBJECT_EXPIRES, fmt.Sprintf("%d", args.ObjectExpires))
		}
		if validFetchMode(args.FetchMode) {
			req.SetHeader(http.BCE_PREFIX+"fetch-mode", args.FetchMode)
		} else {
			if len(args.FetchMode) != 0 {
				return nil, bce.NewBceClientError("invalid fetch mode value: " + args.FetchMode)
			}
		}
		if validStorageClass(args.StorageClass) {
			req.SetHeader(http.BCE_STORAGE_CLASS, args.StorageClass)
		} else {
			if len(args.StorageClass) != 0 {
				return nil, bce.NewBceClientError("invalid storage class value: " +
					args.StorageClass)
			}
		}
		if len(args.FetchCallBackAddress) != 0 {
			req.SetHeader(http.BCE_FETCH_CALLBACK_ADDRESS, args.FetchCallBackAddress)
		}
	}
	// handle options to set the header/params of request
	if err := handleOptions(req, options); err != nil {
		return nil, bce.NewBceClientError(fmt.Sprintf("Handle options occur error: %s", err))
	}
	// Send request and get the result
	resp := &BosResponse{}
	if err := SendRequest(cli, req, resp, ctx); err != nil {
		return nil, err
	}
	if resp.IsFail() {
		return nil, resp.ServiceError()
	}
	jsonBody := &FetchObjectResult{}
	if err := resp.ParseJsonBody(jsonBody); err != nil {
		return nil, err
	}
	return jsonBody, nil
}

// AppendObject - append the given content to a new or existed object which is appendable
//
// PARAMS:
//   - cli: the client agent which can perform sending request
//   - bucket: the bucket name of the object
//   - object: the name of the object
//   - content: the content to be appended
//   - args: the optional arguments to perform the append operation
//
// RETURNS:
//   - *AppendObjectResult: the result status for this api
//   - error: nil if ok otherwise the specific error
func AppendObject(cli bce.Client, bucket, object string, content *bce.Body, args *AppendObjectArgs,
	ctx *BosContext, options ...Option) (*AppendObjectResult, error) {
	req := &BosRequest{}
	req.SetUri(getObjectUri(bucket, object))
	req.SetMethod(http.POST)
	req.SetParam("append", "")
	req.SetBucket(bucket)
	if content == nil {
		return nil, bce.NewBceClientError("AppendObject body should not be emtpy")
	}
	if ctx == nil {
		ctx = newDefaultBosContext()
	}
	if content.Size() >= THRESHOLD_100_CONTINUE {
		req.SetHeader("Expect", "100-continue")
	}
	req.SetBody(content)

	// Optional arguments settings
	if args != nil {
		if args.Offset < 0 {
			return nil, bce.NewBceClientError(
				fmt.Sprintf("invalid append offset value: %d", args.Offset))
		}
		if args.Offset > 0 {
			req.SetParam("offset", fmt.Sprintf("%d", args.Offset))
		}
		setOptionalNullHeaders(req, map[string]string{
			http.CACHE_CONTROL:           args.CacheControl,
			http.CONTENT_DISPOSITION:     args.ContentDisposition,
			http.CONTENT_MD5:             args.ContentMD5,
			http.CONTENT_TYPE:            args.ContentType,
			http.EXPIRES:                 args.Expires,
			http.BCE_CONTENT_SHA256:      args.ContentSha256,
			http.BCE_CONTENT_CRC32:       args.ContentCrc32,
			http.BCE_CONTENT_CRC32C:      args.ContentCrc32c,
			http.BCE_CONTENT_CRC32C_FLAG: strconv.FormatBool(args.ContentCrc32cFlag),
			http.CONTENT_ENCODING:        args.ContentEncoding,
			http.BCE_CONTENT_CRC64ECMA:   args.ContentCrc64ECMA,
		})
		if args.ObjectExpires > 0 {
			req.SetHeader(http.BCE_OBJECT_EXPIRES, fmt.Sprintf("%d", args.ObjectExpires))
		}
		if validStorageClass(args.StorageClass) {
			req.SetHeader(http.BCE_STORAGE_CLASS, args.StorageClass)
		} else {
			if len(args.StorageClass) != 0 {
				return nil, bce.NewBceClientError("invalid storage class value: " +
					args.StorageClass)
			}
		}
		if err := setUserMetadata(req, args.UserMeta); err != nil {
			return nil, err
		}
		//set traffic-limit
		if args.TrafficLimit > 0 {
			if args.TrafficLimit > TRAFFIC_LIMIT_MAX || args.TrafficLimit < TRAFFIC_LIMIT_MIN {
				return nil, bce.NewBceClientError(fmt.Sprintf("TrafficLimit must between %d ~ %d, current value:%d", TRAFFIC_LIMIT_MIN, TRAFFIC_LIMIT_MAX, args.TrafficLimit))
			}
			req.SetHeader(http.BCE_TRAFFIC_LIMIT, fmt.Sprintf("%d", args.TrafficLimit))
		}
	}
	// handle options to set the header/params of request
	if err := handleOptions(req, options); err != nil {
		return nil, bce.NewBceClientError(fmt.Sprintf("Handle options occur error: %s", err))
	}
	// Send request and get the result
	resp := &BosResponse{}
	if err := SendRequest(cli, req, resp, ctx); err != nil {
		return nil, err
	}
	if resp.IsFail() {
		return nil, resp.ServiceError()
	}
	defer func() { resp.Body().Close() }()
	headers := resp.Headers()
	result := &AppendObjectResult{}
	getOptions := []GetOption{
		getHeader(http.CONTENT_MD5, &result.ContentMD5),
		getHeader(http.BCE_CONTENT_CRC32, &result.ContentCrc32),
		getHeader(http.BCE_CONTENT_CRC32C, &result.ContentCrc32c),
		getHeader(http.BCE_CONTENT_CRC64ECMA, &result.ContentCrc64ECMA),
	}
	if err := handleGetOptions(resp, getOptions); err != nil {
		log.Warnf("Handle get options error: %s", err)
	}
	if val, ok := headers[toHttpHeaderKey(http.BCE_NEXT_APPEND_OFFSET)]; ok {
		nextOffset, offsetErr := strconv.ParseInt(val, 10, 64)
		if offsetErr != nil {
			nextOffset = content.Size()
		}
		result.NextAppendOffset = nextOffset
	} else {
		result.NextAppendOffset = content.Size()
	}
	if val, ok := headers[http.ETAG]; ok {
		result.ETag = strings.Trim(val, "\"")
	}
	return result, nil
}

// DeleteObject - delete the given object
//
// PARAMS:
//   - cli: the client agent which can perform sending request
//   - bucket: the bucket name of the object to be deleted
//   - object: the name of the object
//
// RETURNS:
//   - error: nil if ok otherwise the specific error
func DeleteObject(cli bce.Client, bucket, object, versionId string, ctx *BosContext, options ...Option) error {
	req := &BosRequest{}
	req.SetUri(getObjectUri(bucket, object))
	req.SetMethod(http.DELETE)
	if versionId != "" {
		req.SetParam("versionId", versionId)
	}
	if ctx == nil {
		ctx = newDefaultBosContext()
	}
	req.SetBucket(bucket)
	// handle options to set the header/params of request
	if err := handleOptions(req, options); err != nil {
		return bce.NewBceClientError(fmt.Sprintf("Handle options occur error: %s", err))
	}
	resp := &BosResponse{}
	if err := SendRequest(cli, req, resp, ctx); err != nil {
		return err
	}
	if resp.IsFail() {
		return resp.ServiceError()
	}
	defer func() { resp.Body().Close() }()
	return nil
}

// DeleteMultipleObjects - delete the given objects within a single http request
//
// PARAMS:
//   - cli: the client agent which can perform sending request
//   - bucket: the bucket name of the objects to be deleted
//   - objectListStream: the objects list to be delete with json format
//
// RETURNS:
//   - *DeleteMultipleObjectsResult: the objects failed to delete
//   - error: nil if ok otherwise the specific error
func DeleteMultipleObjects(cli bce.Client, bucket string, objectListStream *bce.Body,
	ctx *BosContext, options ...Option) (*DeleteMultipleObjectsResult, error) {
	req := &BosRequest{}
	req.SetUri(getBucketUri(bucket))
	req.SetMethod(http.POST)
	req.SetParam("delete", "")
	req.SetHeader(http.CONTENT_TYPE, "application/json; charset=utf-8")
	req.SetBucket(bucket)
	if objectListStream == nil {
		return nil, bce.NewBceClientError("DeleteMultipleObjects body should not be emtpy")
	}
	if objectListStream.Size() >= THRESHOLD_100_CONTINUE {
		req.SetHeader("Expect", "100-continue")
	}
	if ctx == nil {
		ctx = newDefaultBosContext()
	}
	req.SetBody(objectListStream)
	// handle options to set the header/params of request
	if err := handleOptions(req, options); err != nil {
		return nil, bce.NewBceClientError(fmt.Sprintf("Handle options occur error: %s", err))
	}
	resp := &BosResponse{}
	if err := SendRequest(cli, req, resp, ctx); err != nil {
		return nil, err
	}
	if resp.IsFail() {
		return nil, resp.ServiceError()
	}
	jsonBody := &DeleteMultipleObjectsResult{}

	if resp.Header(http.CONTENT_LENGTH) == "0" {
		resp.Body().Close()
		return jsonBody, nil
	}
	if err := resp.ParseJsonBody(jsonBody); err != nil {
		return nil, err
	}
	return jsonBody, nil
}

// GeneratePresignedUrl - generate an authorization url with expire time and optional arguments
//
// PARAMS:
//     - conf: the client configuration
//     - signer: the client signer object to generate the authorization string
//     - bucket: the target bucket name
//     - object: the target object name
//     - expire: expire time in seconds
//     - method: optional sign method, default is GET
//     - headers: optional sign headers, default just set the Host
//     - params: optional sign params, default is empty
// RETURNS:
//     - string: the presigned url with authorization string

func GeneratePresignedUrl(conf *bce.BceClientConfiguration, signer auth.Signer, bucket,
	object string, expire int, method string, headers, params map[string]string) string {
	return GeneratePresignedUrlInternal(conf, signer, bucket, object, expire, method, headers, params, false)
}
func GeneratePresignedUrlPathStyle(conf *bce.BceClientConfiguration, signer auth.Signer, bucket,
	object string, expire int, method string, headers, params map[string]string) string {
	return GeneratePresignedUrlInternal(conf, signer, bucket, object, expire, method, headers, params, true)
}

func GeneratePresignedUrlInternal(conf *bce.BceClientConfiguration, signer auth.Signer, bucket,
	object string, expire int, method string, headers, params map[string]string, path_style bool) string {
	req := &bce.BceRequest{}
	// Set basic arguments
	if len(method) == 0 {
		method = http.GET
	}
	objectTrimSlash := strings.Trim(object, "/")
	if method == http.GET && objectTrimSlash == "" {
		log.Warnf("objectKey is empty, cannot generate presigned url.")
		return ""
	}
	if !path_style && method == http.GET && objectTrimSlash == "v1" {
		log.Warnf("objectKey '%s' is invalid, cannot generate presigned url.", object)
		return ""
	}
	req.SetMethod(method)
	req.SetEndpoint(conf.Endpoint)
	if req.Protocol() == "" {
		req.SetProtocol(bce.DEFAULT_PROTOCOL)
	}
	domain := req.Host()
	if pos := strings.Index(domain, ":"); pos != -1 {
		domain = domain[:pos]
	}
	if path_style {
		req.SetUri(getObjectUri(bucket, object))
		if conf.CnameEnabled || isCnameLikeHost(conf.Endpoint) {
			req.SetUri(getCnameUri(req.Uri()))
		}
	} else {
		if len(bucket) != 0 && net.ParseIP(domain) == nil { // not use an IP as the endpoint by client
			req.SetUri(bce.URI_PREFIX + object)
			if !conf.CnameEnabled && !isCnameLikeHost(conf.Endpoint) {
				req.SetHost(bucket + "." + req.Host())
			}
		} else {
			req.SetUri(getObjectUri(bucket, object))
			if conf.CnameEnabled || isCnameLikeHost(conf.Endpoint) {
				req.SetUri(getCnameUri(req.Uri()))
			}
		}
	}
	// Set headers and params if given.
	req.SetHeader(http.HOST, req.Host())
	for k, v := range headers {
		req.SetHeader(k, v)
	}
	for k, v := range params {
		req.SetParam(k, v)
	}
	// Copy one SignOptions object to rewrite it.
	option := *conf.SignOption
	if expire != 0 {
		option.ExpireSeconds = expire
	}

	if conf.Credentials.SessionToken != "" {
		req.SetParam(http.BCE_SECURITY_TOKEN, conf.Credentials.SessionToken)
	}

	// Generate the authorization string and return the signed url.
	signer.Sign(&req.Request, conf.Credentials, &option)
	req.SetParam("authorization", req.Header(http.AUTHORIZATION))
	return fmt.Sprintf("%s://%s%s?%s", req.Protocol(), req.Host(),
		util.UriEncode(req.Uri(), false), req.QueryString())
}

// PutObjectAcl - set the ACL of the given object
//
// PARAMS:
//   - cli: the client agent which can perform sending request
//   - bucket: the bucket name
//   - object: the object name
//   - cannedAcl: support private and public-read
//   - grantRead: user id list
//   - grantFullControl: user id list
//   - aclBody: the acl file body
//
// RETURNS:
//   - error: nil if success otherwise the specific error
func PutObjectAcl(cli bce.Client, bucket, object, cannedAcl string, grantRead, grantFullControl []string,
	aclBody *bce.Body, ctx *BosContext, options ...Option) error {
	req := &BosRequest{}
	req.SetUri(getObjectUri(bucket, object))
	req.SetMethod(http.PUT)
	req.SetParam("acl", "")
	req.SetBucket(bucket)
	if ctx == nil {
		ctx = newDefaultBosContext()
	}
	// Choose a acl setting method
	methods := 0
	if len(cannedAcl) != 0 {
		methods += 1
		if validCannedAcl(cannedAcl) {
			req.SetHeader(http.BCE_ACL, cannedAcl)
		}
	}
	if len(grantRead) != 0 {
		methods += 1
		req.SetHeader(http.BCE_GRANT_READ, joinUserIds(grantRead))
	}
	if len(grantFullControl) != 0 {
		methods += 1
		req.SetHeader(http.BCE_GRANT_FULL_CONTROL, joinUserIds(grantFullControl))
	}
	if aclBody != nil {
		methods += 1
		req.SetHeader(http.CONTENT_TYPE, bce.DEFAULT_CONTENT_TYPE)
		req.SetBody(aclBody)
	}
	if methods != 1 {
		return bce.NewBceClientError("BOS only support one acl setting method at the same time")
	}
	// handle options to set the header/params of request
	if err := handleOptions(req, options); err != nil {
		return bce.NewBceClientError(fmt.Sprintf("Handle options occur error: %s", err))
	}
	// Do sending request
	resp := &BosResponse{}
	if err := SendRequest(cli, req, resp, ctx); err != nil {
		return err
	}
	if resp.IsFail() {
		return resp.ServiceError()
	}
	defer func() { resp.Body().Close() }()
	return nil
}

// GetObjectAcl - get the ACL of the given object
//
// PARAMS:
//   - cli: the client agent which can perform sending request
//   - bucket: the bucket name
//   - object: the object name
//
// RETURNS:
//   - result: the object acl result object
//   - error: nil if success otherwise the specific error
func GetObjectAcl(cli bce.Client, bucket, object string, ctx *BosContext,
	options ...Option) (*GetObjectAclResult, error) {
	req := &BosRequest{}
	req.SetUri(getObjectUri(bucket, object))
	req.SetMethod(http.GET)
	req.SetParam("acl", "")
	req.SetBucket(bucket)
	if ctx == nil {
		ctx = newDefaultBosContext()
	}
	// handle options to set the header/params of request
	if err := handleOptions(req, options); err != nil {
		return nil, bce.NewBceClientError(fmt.Sprintf("Handle options occur error: %s", err))
	}
	resp := &BosResponse{}
	if err := SendRequest(cli, req, resp, ctx); err != nil {
		return nil, err
	}
	if resp.IsFail() {
		return nil, resp.ServiceError()
	}
	result := &GetObjectAclResult{}
	if err := resp.ParseJsonBody(result); err != nil {
		return nil, err
	}
	return result, nil
}

// DeleteObjectAcl - delete the ACL of the given object
//
// PARAMS:
//   - cli: the client agent which can perform sending request
//   - bucket: the bucket name
//   - object: the object name
//
// RETURNS:
//   - error: nil if success otherwise the specific error
func DeleteObjectAcl(cli bce.Client, bucket, object string, ctx *BosContext, options ...Option) error {
	req := &BosRequest{}
	req.SetUri(getObjectUri(bucket, object))
	req.SetMethod(http.DELETE)
	req.SetParam("acl", "")
	req.SetBucket(bucket)
	if ctx == nil {
		ctx = newDefaultBosContext()
	}
	// handle options to set the header/params of request
	if err := handleOptions(req, options); err != nil {
		return bce.NewBceClientError(fmt.Sprintf("Handle options occur error: %s", err))
	}
	resp := &BosResponse{}
	if err := SendRequest(cli, req, resp, ctx); err != nil {
		return err
	}
	if resp.IsFail() {
		return resp.ServiceError()
	}
	defer func() { resp.Body().Close() }()
	return nil
}

// RestoreObject - restore the archive object
//
// PARAMS:
//   - cli: the client agent which can perform sending request
//   - bucket: the bucket name
//   - object: the object name
//   - args: the restore args
//
// RETURNS:
//   - error: nil if success otherwise the specific error
func RestoreObject(cli bce.Client, bucket string, object string, args ArchiveRestoreArgs,
	ctx *BosContext, options ...Option) error {
	req := &BosRequest{}
	req.SetUri(getObjectUri(bucket, object))
	req.SetParam("restore", "")
	req.SetMethod(http.POST)
	req.SetHeader(http.BCE_RESTORE_DAYS, strconv.Itoa(args.RestoreDays))
	req.SetHeader(http.BCE_RESTORE_TIER, args.RestoreTier)
	req.SetBucket(bucket)
	if ctx == nil {
		ctx = newDefaultBosContext()
	}
	// handle options to set the header/params of request
	if err := handleOptions(req, options); err != nil {
		return bce.NewBceClientError(fmt.Sprintf("Handle options occur error: %s", err))
	}
	resp := &BosResponse{}
	if err := SendRequest(cli, req, resp, ctx); err != nil {
		return err
	}
	if resp.IsFail() {
		return resp.ServiceError()
	}

	return nil
}

// PutObjectSymlink - put the object from the string or the stream
//
// PARAMS:
//   - cli: the client agent which can perform sending request
//   - bucket: the bucket name of the object
//   - object: the name of the object
//   - symlinkKey: the name of the symlink
//   - symlinkArgs: the optional arguments of this api
//
// RETURNS:
//   - error: nil if ok otherwise the specific error
func PutObjectSymlink(cli bce.Client, bucket string, object string, symlinkKey string,
	symlinkArgs *PutSymlinkArgs, ctx *BosContext, options ...Option) error {
	req := &BosRequest{}
	req.SetUri(getObjectUri(bucket, symlinkKey))
	req.SetParam("symlink", "")
	req.SetMethod(http.PUT)
	req.SetBucket(bucket)
	if ctx == nil {
		ctx = newDefaultBosContext()
	}
	if symlinkArgs != nil {
		if len(symlinkArgs.ForbidOverwrite) != 0 {
			if !validForbidOverwrite(symlinkArgs.ForbidOverwrite) {
				return bce.NewBceClientError("invalid forbid overwrite val," + symlinkArgs.ForbidOverwrite)
			}
			req.SetHeader(http.BCE_FORBID_OVERWRITE, symlinkArgs.ForbidOverwrite)
		}

		if len(symlinkArgs.StorageClass) != 0 {
			if !validStorageClass(symlinkArgs.StorageClass) {
				return bce.NewBceClientError("invalid storage class val," + symlinkArgs.StorageClass)
			}
			if symlinkArgs.StorageClass == STORAGE_CLASS_ARCHIVE {
				return bce.NewBceClientError("archive storage class not support")
			}
			req.SetHeader(http.BCE_STORAGE_CLASS, symlinkArgs.StorageClass)
		}

		if len(symlinkArgs.UserMeta) != 0 {
			if err := setUserMetadata(req, symlinkArgs.UserMeta); err != nil {
				return err
			}
		}
		if len(symlinkArgs.SymlinkBucket) != 0 {
			req.SetHeader(http.BCE_SYMLINK_BUCKET, symlinkArgs.SymlinkBucket)
		}
	}
	req.SetHeader(http.BCE_SYMLINK_TARGET, object)
	// handle options to set the header/params of request
	if err := handleOptions(req, options); err != nil {
		return bce.NewBceClientError(fmt.Sprintf("Handle options occur error: %s", err))
	}
	resp := &BosResponse{}
	if err := SendRequest(cli, req, resp, ctx); err != nil {
		return err
	}
	if resp.IsFail() {
		return resp.ServiceError()
	}
	defer func() { resp.Body().Close() }()
	return nil
}

// PutObjectSymlink - put the object from the string or the stream
//
// PARAMS:
//   - cli: the client agent which can perform sending request
//   - bucket: the bucket name of the object
//   - symlinkKey: the name of the symlink
//
// RETURNS:
//   - string: the name of the target object
//   - error: nil if ok otherwise the specific error
func GetObjectSymlink(cli bce.Client, bucket string, symlinkKey string,
	ctx *BosContext, options ...Option) (string, error) {
	req := &BosRequest{}
	req.SetUri(getObjectUri(bucket, symlinkKey))
	req.SetParam("symlink", "")
	req.SetMethod(http.GET)
	req.SetBucket(bucket)
	if ctx == nil {
		ctx = newDefaultBosContext()
	}
	// handle options to set the header/params of request
	if err := handleOptions(req, options); err != nil {
		return "", bce.NewBceClientError(fmt.Sprintf("Handle options occur error: %s", err))
	}
	resp := &BosResponse{}
	if err := SendRequest(cli, req, resp, ctx); err != nil {
		return "", err
	}
	if resp.IsFail() {
		return "", resp.ServiceError()
	}
	defer func() { resp.Body().Close() }()
	if resp.Header(http.BCE_SYMLINK_BUCKET) != "" {
		result := BOS_CONFIG_PREFIX + resp.Header(http.BCE_SYMLINK_BUCKET) + "/" + resp.Header(http.BCE_SYMLINK_TARGET)
		return result, nil
	}
	return resp.Header(http.BCE_SYMLINK_TARGET), nil
}

// PutObjectTag - set tag for given object
//
// PARAMS:
//     - cli: the client agent which can perform sending request
//     - bucket: the bucket name of the object
//     - object: the name of the object
//     - putObjectTagArgs: the arguments of object tag
// RETURNS:
//     - error: nil if ok otherwise the specific error

func PutObjectTag(cli bce.Client, bucket, object string, putObjectTagArgs *PutObjectTagArgs,
	ctx *BosContext, options ...Option) error {
	req := &BosRequest{}
	req.SetUri(getObjectUri(bucket, object))
	req.SetMethod(http.PUT)
	req.SetParam("tagging", "")
	req.SetBucket(bucket)
	reqByte, _ := json.Marshal(putObjectTagArgs)
	body, err := bce.NewBodyFromString(string(reqByte))
	if err != nil {
		return err
	}
	req.SetBody(body)
	if ctx == nil {
		ctx = newDefaultBosContext()
	}
	// handle options to set the header/params of request
	if err := handleOptions(req, options); err != nil {
		return bce.NewBceClientError(fmt.Sprintf("Handle options occur error: %s", err))
	}
	resp := &BosResponse{}
	if err := SendRequest(cli, req, resp, ctx); err != nil {
		return err
	}
	if resp.IsFail() {
		return resp.ServiceError()
	}
	defer func() { resp.Body().Close() }()
	return nil
}

func GetObjectTag(cli bce.Client, bucket, object string, ctx *BosContext,
	options ...Option) (map[string]interface{}, error) {
	req := &BosRequest{}
	req.SetUri(getObjectUri(bucket, object))
	req.SetMethod(http.GET)
	req.SetParam("tagging", "")
	req.SetBucket(bucket)
	if ctx == nil {
		ctx = newDefaultBosContext()
	}
	// handle options to set the header/params of request
	if err := handleOptions(req, options); err != nil {
		return nil, bce.NewBceClientError(fmt.Sprintf("Handle options occur error: %s", err))
	}
	resp := &BosResponse{}
	if err := SendRequest(cli, req, resp, ctx); err != nil {
		return nil, err
	}
	if resp.IsFail() {
		return nil, resp.ServiceError()
	}
	defer func() { resp.Body().Close() }()
	bodyBytes, err := ioutil.ReadAll(resp.Body())
	if err != nil {
		return nil, err
	}

	result, err := ParseObjectTagResult(bodyBytes)
	if err != nil {
		return nil, err
	}
	return result, nil
}

func DeleteObjectTag(cli bce.Client, bucket, object string, ctx *BosContext, options ...Option) error {
	req := &BosRequest{}
	req.SetUri(getObjectUri(bucket, object))
	req.SetMethod(http.DELETE)
	req.SetParam("tagging", "")
	req.SetBucket(bucket)
	if ctx == nil {
		ctx = newDefaultBosContext()
	}
	// handle options to set the header/params of request
	if err := handleOptions(req, options); err != nil {
		return bce.NewBceClientError(fmt.Sprintf("Handle options occur error: %s", err))
	}
	resp := &BosResponse{}
	if err := SendRequest(cli, req, resp, ctx); err != nil {
		return err
	}
	if resp.IsFail() {
		return resp.ServiceError()
	}
	defer func() { resp.Body().Close() }()
	return nil
}
