// Package commitmgr manages commit objects.
package commitmgr

import (
	"bytes"
	"crypto/sha1"
	"encoding/binary"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"time"

	"github.com/haiwen/seafile-server/fileserver/objstore"
	"github.com/haiwen/seafile-server/fileserver/utils"
)

// Commit is a commit object
type Commit struct {
	CommitID       string `json:"commit_id"`
	RepoID         string `json:"repo_id"`
	RootID         string `json:"root_id"`
	CreatorName    string `json:"creator_name,omitempty"`
	CreatorID      string `json:"creator"`
	Desc           string `json:"description"`
	Ctime          int64  `json:"ctime"`
	ParentID       String `json:"parent_id"`
	SecondParentID String `json:"second_parent_id"`
	RepoName       string `json:"repo_name"`
	RepoDesc       string `json:"repo_desc"`
	RepoCategory   string `json:"repo_category"`
	DeviceName     string `json:"device_name,omitempty"`
	ClientVersion  string `json:"client_version,omitempty"`
	Encrypted      string `json:"encrypted,omitempty"`
	EncVersion     int    `json:"enc_version,omitempty"`
	Magic          string `json:"magic,omitempty"`
	RandomKey      string `json:"key,omitempty"`
	Salt           string `json:"salt,omitempty"`
	Version        int    `json:"version,omitempty"`
	Conflict       int    `json:"conflict,omitempty"`
	NewMerge       int    `json:"new_merge,omitempty"`
	Repaired       int    `json:"repaired,omitempty"`
}

var store *objstore.ObjectStore

// Init initializes commit manager and creates underlying object store.
func Init(seafileConfPath string, seafileDataDir string) {
	store = objstore.New(seafileConfPath, seafileDataDir, "commits")
}

// NewCommit initializes a Commit object.
func NewCommit(repoID, parentID, newRoot, user, desc string) *Commit {
	commit := new(Commit)
	commit.RepoID = repoID
	commit.RootID = newRoot
	commit.Desc = desc
	commit.CreatorName = user
	commit.CreatorID = "0000000000000000000000000000000000000000"
	commit.Ctime = time.Now().Unix()
	commit.CommitID = computeCommitID(commit)
	if parentID != "" {
		commit.ParentID.SetValid(parentID)
	}

	return commit
}

func computeCommitID(commit *Commit) string {
	hash := sha1.New()
	hash.Write([]byte(commit.RootID))
	hash.Write([]byte(commit.CreatorID))
	hash.Write([]byte(commit.CreatorName))
	hash.Write([]byte(commit.Desc))
	tmpBuf := make([]byte, 8)
	binary.BigEndian.PutUint64(tmpBuf, uint64(commit.Ctime))
	hash.Write(tmpBuf)

	checkSum := hash.Sum(nil)
	id := hex.EncodeToString(checkSum[:])

	return id
}

// FromData reads from p and converts JSON-encoded data to commit.
func (commit *Commit) FromData(p []byte) error {
	err := json.Unmarshal(p, commit)
	if err != nil {
		return err
	}

	if !utils.IsValidUUID(commit.RepoID) {
		return fmt.Errorf("repo id %s is invalid", commit.RepoID)
	}
	if !utils.IsObjectIDValid(commit.RootID) {
		return fmt.Errorf("root id %s is invalid", commit.RootID)
	}
	if len(commit.CreatorID) != 40 {
		return fmt.Errorf("creator id %s is invalid", commit.CreatorID)
	}
	if commit.ParentID.Valid && !utils.IsObjectIDValid(commit.ParentID.String) {
		return fmt.Errorf("parent id %s is invalid", commit.ParentID.String)
	}
	if commit.SecondParentID.Valid && !utils.IsObjectIDValid(commit.SecondParentID.String) {
		return fmt.Errorf("second parent id %s is invalid", commit.SecondParentID.String)
	}

	return nil
}

// ToData converts commit to JSON-encoded data and writes to w.
func (commit *Commit) ToData(w io.Writer) error {
	jsonstr, err := json.Marshal(commit)
	if err != nil {
		return err
	}

	_, err = w.Write(jsonstr)
	if err != nil {
		return err
	}

	return nil
}

// ReadRaw reads data in binary format from storage backend.
func ReadRaw(repoID string, commitID string, w io.Writer) error {
	err := store.Read(repoID, commitID, w)
	if err != nil {
		return err
	}
	return nil
}

// WriteRaw writes data in binary format to storage backend.
func WriteRaw(repoID string, commitID string, r io.Reader) error {
	err := store.Write(repoID, commitID, r, false)
	if err != nil {
		return err
	}
	return nil
}

// Load commit from storage backend.
func Load(repoID string, commitID string) (*Commit, error) {
	var buf bytes.Buffer
	commit := new(Commit)
	err := ReadRaw(repoID, commitID, &buf)
	if err != nil {
		return nil, err
	}
	err = commit.FromData(buf.Bytes())
	if err != nil {
		return nil, err
	}

	return commit, nil
}

// Save commit to storage backend.
func Save(commit *Commit) error {
	var buf bytes.Buffer
	err := commit.ToData(&buf)
	if err != nil {
		return err
	}

	err = WriteRaw(commit.RepoID, commit.CommitID, &buf)
	if err != nil {
		return err
	}

	return err
}

// Exists checks commit if exists.
func Exists(repoID string, commitID string) (bool, error) {
	return store.Exists(repoID, commitID)
}