package api

import (
	"errors"
	"io"
	"io/ioutil"
	"net/http"
	"sync"

	"code.google.com/p/google-api-go-client/googleapi"
	"code.google.com/p/google-api-go-client/storage/v1"
)

var (
	ErrBucketNotFound = errors.New("bucket not found")
	ErrBlobNotFound   = errors.New("blob not found")
)

type Storage interface {
	Upload(bucket, tmp string, r io.Reader) error
	Delete(bucket, tmp string) error
	Move(srcBucket, src, dstBucket, dst string) error
	Download(bucket, id string, w io.Writer) (int64, error)
}

type memstorage struct {
	buckets map[string]membucket
	sync.RWMutex
}

type membucket map[string][]byte

func NewMemStorage() *memstorage {
	return &memstorage{
		buckets: map[string]membucket{},
	}
}

func (m *memstorage) Upload(bucket, tmp string, r io.Reader) error {
	m.Lock()
	defer m.Unlock()

	if _, ok := m.buckets[bucket]; !ok {
		m.buckets[bucket] = membucket{}
	}
	bytes, err := ioutil.ReadAll(r)
	if err != nil {
		return err
	}
	m.buckets[bucket][tmp] = bytes
	return nil
}

func (m *memstorage) Delete(bucket, tmp string) error {
	m.Lock()
	defer m.Unlock()

	delete(m.buckets[bucket], tmp)
	return nil
}

func (m *memstorage) Move(srcBucket, src, dstBucket, dst string) error {
	m.Lock()
	defer m.Unlock()

	if _, ok := m.buckets[srcBucket]; !ok {
		return ErrBucketNotFound
	}
	if _, ok := m.buckets[srcBucket][src]; !ok {
		return ErrBlobNotFound
	}
	if _, ok := m.buckets[dstBucket]; !ok {
		m.buckets[dstBucket] = membucket{}
	}
	m.buckets[dstBucket][dst] = m.buckets[srcBucket][src]
	return m.Delete(srcBucket, src)
}

func (m *memstorage) Download(bucket, id string, w io.Writer) (int64, error) {
	m.RLock()
	defer m.RUnlock()

	if _, ok := m.buckets[bucket]; !ok {
		return 0, ErrBucketNotFound
	}
	if _, ok := m.buckets[bucket][id]; !ok {
		return 0, ErrBlobNotFound
	}
	n, err := w.Write(m.buckets[bucket][id])
	return int64(n), err
}

type GoogleCloudStorage struct {
	*storage.Service
}

func (gcs *GoogleCloudStorage) Upload(bucket, tmp string, r io.Reader) error {
	object := &storage.Object{Name: tmp}
	_, err := gcs.Objects.Insert(bucket, object).Media(r).Do()
	if err != nil {
		// TODO: abstract out this error
		return err
	}
	return nil
}

func (gcs *GoogleCloudStorage) Delete(bucket, tmp string) error {
	// TODO: abstract out this error
	return gcs.Objects.Delete(bucket, tmp).Do()
}

func (gcs *GoogleCloudStorage) Move(srcBucket, src, dstBucket, dst string) error {
	_, err := gcs.Objects.Get(dstBucket, dst).Do()
	if err == nil {
		return nil
	}
	// ignore 404 errors on the destination; we don't expect it to exist, after all
	if e, ok := err.(*googleapi.Error); !ok || e.Code != 404 {
		// TODO: abstract out this error
		return err
	}
	_, err = gcs.Objects.Copy(srcBucket, src, dstBucket, dst, nil).Do()
	if err != nil {
		// TODO: abstract out this error
		return err
	}
	objectAcl := &storage.ObjectAccessControl{
		Bucket: dstBucket, Entity: "allUsers", Object: dst, Role: "READER",
	}
	_, err = gcs.ObjectAccessControls.Insert(dstBucket, dst, objectAcl).Do()
	if err != nil {
		// TODO: abstract out this error
		return err
	}
	return nil
}

func (gcs *GoogleCloudStorage) Download(bucket, id string, w io.Writer) (int64, error) {
	res, err := gcs.Objects.Get(bucket, id).Do()
	if err != nil {
		// TODO: abstract out this error
		return 0, err
	}
	resp, err := http.Get(res.MediaLink)
	if err != nil {
		// TODO: abstract out this error
		return 0, err
	}
	defer resp.Body.Close()
	// TODO: abstract out this error
	return io.Copy(w, resp.Body)
}
