package stream

import (
	"bytes"
	"crypto/aes"
	"crypto/rand"
	"crypto/sha512"
	"encoding/hex"
	"encoding/json"
	"path"
	"regexp"
	"strconv"
	"strings"
)

const streamTypeLBRYFile = "lbryfile"
const defaultSanitizedFilename = "lbry_download"

// BlobInfo is the stream descriptor info for a single blob in a stream
// Encoding to and from JSON is customized to match existing behavior (see json.go in package)
type BlobInfo struct {
	Length   int    `json:"length"`
	BlobNum  int    `json:"blob_num"`
	BlobHash []byte `json:"-"`
	IV       []byte `json:"-"`
}

// Hash returns the hash of the blob info for calculating the stream hash
func (bi BlobInfo) Hash() []byte {
	sum := sha512.New384()
	if bi.Length > 0 {
		sum.Write([]byte(hex.EncodeToString(bi.BlobHash)))
	}
	sum.Write([]byte(strconv.Itoa(bi.BlobNum)))
	sum.Write([]byte(hex.EncodeToString(bi.IV)))
	sum.Write([]byte(strconv.Itoa(bi.Length)))
	return sum.Sum(nil)
}

// SDBlob contains information about the rest of the blobs in the stream
// NOTE: Encoding to and from JSON is customized to match existing behavior (see json.go in package)
type SDBlob struct {
	StreamName        string     `json:"-"` // shadowed by JSONSDBlob in json.go
	BlobInfos         []BlobInfo `json:"blobs"`
	StreamType        string     `json:"stream_type"`
	Key               []byte     `json:"-"` // shadowed by JSONSDBlob in json.go
	SuggestedFileName string     `json:"-"` // shadowed by JSONSDBlob in json.go
	StreamHash        []byte     `json:"-"` // shadowed by JSONSDBlob in json.go
}

// Hash returns a hash of the SD blob data
func (s SDBlob) Hash() []byte {
	hashBytes := sha512.Sum384(s.ToBlob())
	return hashBytes[:]
}

// HashHex returns the SD blob hash as a hex string
func (s SDBlob) HashHex() string {
	return hex.EncodeToString(s.Hash())
}

// ToJson returns the SD blob hash as JSON
func (s SDBlob) ToJson() string {
	j, err := json.MarshalIndent(s, "", "  ")
	if err != nil {
		panic(err)
	}
	return string(j)
}

// ToBlob converts the SDBlob to a normal data Blob
func (s SDBlob) ToBlob() Blob {
	jsonSD, err := json.Marshal(s)
	if err != nil {
		panic(err)
	}

	// COMPATIBILITY HACK to make json output match python's json. this can be
	// removed when we implement canonical JSON encoding
	jsonSD = []byte(strings.Replace(string(jsonSD), ",", ", ", -1))
	jsonSD = []byte(strings.Replace(string(jsonSD), ":", ": ", -1))

	return jsonSD
}

// FromBlob unmarshals a data Blob that should contain SDBlob data
func (s *SDBlob) FromBlob(b Blob) error {
	return json.Unmarshal(b, s)
}

// addBlob adds the blob's info to stream
func (s *SDBlob) addBlob(b Blob, iv []byte) {
	if len(iv) == 0 {
		panic("empty IV")
	}
	s.BlobInfos = append(s.BlobInfos, BlobInfo{
		BlobNum:  len(s.BlobInfos),
		Length:   b.Size(),
		BlobHash: b.Hash(),
		IV:       iv,
	})
}

// IsValid returns true if the set StreamHash matches the current hash of the stream data
func (s SDBlob) IsValid() bool {
	return bytes.Equal(s.StreamHash, s.computeStreamHash())
}

// updateStreamHash sets the stream hash to the current hash of the stream data
func (s *SDBlob) updateStreamHash() {
	s.StreamHash = s.computeStreamHash()
}

// computeStreamHash calculates the stream hash for the stream
func (s *SDBlob) computeStreamHash() []byte {
	return streamHash(
		hex.EncodeToString([]byte(s.StreamName)),
		hex.EncodeToString(s.Key),
		hex.EncodeToString([]byte(s.SuggestedFileName)),
		s.BlobInfos,
	)
}

func (s SDBlob) fileSize() int {
	size := 0
	for _, bi := range s.BlobInfos {
		size += bi.Length
	}
	return size
}

// streamHash calculates the stream hash, given the stream's fields and blobs
func streamHash(hexStreamName, hexKey, hexSuggestedFileName string, blobInfos []BlobInfo) []byte {
	blobSum := sha512.New384()
	for _, b := range blobInfos {
		blobSum.Write(b.Hash())
	}

	sum := sha512.New384()
	sum.Write([]byte(hexStreamName))
	sum.Write([]byte(hexKey))
	sum.Write([]byte(hexSuggestedFileName))
	sum.Write(blobSum.Sum(nil))
	return sum.Sum(nil)
}

// randIV returns a random AES IV
func randIV() []byte {
	iv := make([]byte, aes.BlockSize)
	_, err := rand.Read(iv)
	if err != nil {
		panic("failed to make random iv")
	}
	return iv
}

// NullIV returns an IV of 0s
func NullIV() []byte {
	return make([]byte, aes.BlockSize)
}

var illegalFilenameChars = regexp.MustCompile(`(` +
	`[<>:"/\\|?*]+|` + // Illegal characters
	`[\x00-\x1F]+|` + // All characters in range 0-31
	`[ \t]*(\.)+[ \t]*$|` + // Dots at the end
	`(^[ \t]+|[ \t]+$)|` + // Leading and trailing whitespace
	`^CON$|^PRN$|^AUX$|` + // Illegal names on windows
	`^NUL$|^COM[1-9]$|^LPT[1-9]$` + // Illegal names on windows
	`)`)

// sanitizeFilename cleans a filename so it can go into an sd blob
// python implementation: https://github.com/lbryio/lbry-sdk/blob/e89acac235f497b0215991d5142aa678d525eb59/lbry/stream/descriptor.py#L69
func sanitizeFilename(name string) string {
	//defaultFilename := "lbry_download"

	ext := path.Ext(name)
	name = name[:len(name)-len(ext)]

	if name == "" && ext != "" {
		// python does it this way. I think it's weird, but we should try and match them
		name = ext
		ext = ""
	}

	name = illegalFilenameChars.ReplaceAllString(name, "")
	ext = illegalFilenameChars.ReplaceAllString(ext, "")

	if name == "" {
		name = defaultSanitizedFilename
	}

	if len(ext) > 1 {
		name += ext
	}

	return name
}