package operations

import (
	"path"
	"time"

	"github.com/ncw/rclone/backend/crypt"
	"github.com/ncw/rclone/fs"
	"github.com/ncw/rclone/fs/walk"
	"github.com/pkg/errors"
)

// ListJSONItem in the struct which gets marshalled for each line
type ListJSONItem struct {
	Path      string
	Name      string
	Encrypted string `json:",omitempty"`
	Size      int64
	MimeType  string    `json:",omitempty"`
	ModTime   Timestamp //`json:",omitempty"`
	IsDir     bool
	Hashes    map[string]string `json:",omitempty"`
	ID        string            `json:",omitempty"`
	OrigID    string            `json:",omitempty"`
}

// Timestamp a time in the provided format
type Timestamp struct {
	When   time.Time
	Format string
}

// MarshalJSON turns a Timestamp into JSON
func (t Timestamp) MarshalJSON() (out []byte, err error) {
	if t.When.IsZero() {
		return []byte(`""`), nil
	}
	return []byte(`"` + t.When.Format(t.Format) + `"`), nil
}

// Returns a time format for the given precision
func formatForPrecision(precision time.Duration) string {
	switch {
	case precision <= time.Nanosecond:
		return "2006-01-02T15:04:05.000000000Z07:00"
	case precision <= 10*time.Nanosecond:
		return "2006-01-02T15:04:05.00000000Z07:00"
	case precision <= 100*time.Nanosecond:
		return "2006-01-02T15:04:05.0000000Z07:00"
	case precision <= time.Microsecond:
		return "2006-01-02T15:04:05.000000Z07:00"
	case precision <= 10*time.Microsecond:
		return "2006-01-02T15:04:05.00000Z07:00"
	case precision <= 100*time.Microsecond:
		return "2006-01-02T15:04:05.0000Z07:00"
	case precision <= time.Millisecond:
		return "2006-01-02T15:04:05.000Z07:00"
	case precision <= 10*time.Millisecond:
		return "2006-01-02T15:04:05.00Z07:00"
	case precision <= 100*time.Millisecond:
		return "2006-01-02T15:04:05.0Z07:00"
	}
	return time.RFC3339
}

// ListJSONOpt describes the options for ListJSON
type ListJSONOpt struct {
	Recurse       bool `json:"recurse"`
	NoModTime     bool `json:"noModTime"`
	ShowEncrypted bool `json:"showEncrypted"`
	ShowOrigIDs   bool `json:"showOrigIDs"`
	ShowHash      bool `json:"showHash"`
}

// ListJSON lists fsrc using the options in opt calling callback for each item
func ListJSON(fsrc fs.Fs, remote string, opt *ListJSONOpt, callback func(*ListJSONItem) error) error {
	var cipher crypt.Cipher
	if opt.ShowEncrypted {
		fsInfo, _, _, config, err := fs.ConfigFs(fsrc.Name() + ":" + fsrc.Root())
		if err != nil {
			return errors.Wrap(err, "ListJSON failed to load config for crypt remote")
		}
		if fsInfo.Name != "crypt" {
			return errors.New("The remote needs to be of type \"crypt\"")
		}
		cipher, err = crypt.NewCipher(config)
		if err != nil {
			return errors.Wrap(err, "ListJSON failed to make new crypt remote")
		}
	}
	format := formatForPrecision(fsrc.Precision())
	err := walk.Walk(fsrc, remote, false, ConfigMaxDepth(opt.Recurse), func(dirPath string, entries fs.DirEntries, err error) error {
		if err != nil {
			fs.CountError(err)
			fs.Errorf(dirPath, "error listing: %v", err)
			return nil
		}
		for _, entry := range entries {
			item := ListJSONItem{
				Path:     entry.Remote(),
				Name:     path.Base(entry.Remote()),
				Size:     entry.Size(),
				MimeType: fs.MimeTypeDirEntry(entry),
			}
			if !opt.NoModTime {
				item.ModTime = Timestamp{When: entry.ModTime(), Format: format}
			}
			if cipher != nil {
				switch entry.(type) {
				case fs.Directory:
					item.Encrypted = cipher.EncryptDirName(path.Base(entry.Remote()))
				case fs.Object:
					item.Encrypted = cipher.EncryptFileName(path.Base(entry.Remote()))
				default:
					fs.Errorf(nil, "Unknown type %T in listing", entry)
				}
			}
			if do, ok := entry.(fs.IDer); ok {
				item.ID = do.ID()
			}
			if opt.ShowOrigIDs {
				cur := entry
				for {
					u, ok := cur.(fs.ObjectUnWrapper)
					if !ok {
						break // not a wrapped object, use current id
					}
					next := u.UnWrap()
					if next == nil {
						break // no base object found, use current id
					}
					cur = next
				}
				if do, ok := cur.(fs.IDer); ok {
					item.OrigID = do.ID()
				}
			}
			switch x := entry.(type) {
			case fs.Directory:
				item.IsDir = true
			case fs.Object:
				item.IsDir = false
				if opt.ShowHash {
					item.Hashes = make(map[string]string)
					for _, hashType := range x.Fs().Hashes().Array() {
						hash, err := x.Hash(hashType)
						if err != nil {
							fs.Errorf(x, "Failed to read hash: %v", err)
						} else if hash != "" {
							item.Hashes[hashType.String()] = hash
						}
					}
				}
			default:
				fs.Errorf(nil, "Unknown type %T in listing in ListJSON", entry)
			}
			err = callback(&item)
			if err != nil {
				return errors.Wrap(err, "callback failed in ListJSON")
			}

		}
		return nil
	})
	if err != nil {
		return errors.Wrap(err, "error in ListJSON")
	}
	return nil
}