package sm9

import (
	"crypto/cipher"
	"io"

	_cipher "github.com/emmansun/gmsm/cipher"
	"github.com/emmansun/gmsm/internal/subtle"
	"github.com/emmansun/gmsm/padding"
	"github.com/emmansun/gmsm/sm4"
)

// EncrypterOpts is an interface implemented by detail encrypt/decrypt mode.
type EncrypterOpts interface {
	// GetEncryptType returns the encrypt type/mode.
	GetEncryptType() encryptType
	// GetKeySize returns key size used by this encrypt mode.
	GetKeySize(plaintext []byte) int
	// Encrypt encrypts the plaintext with the key, returns ciphertext.
	Encrypt(rand io.Reader, key, plaintext []byte) ([]byte, error)
	// Decrypt decrypts the ciphertext with the key, returns plaintext.
	Decrypt(key, ciphertext []byte) ([]byte, error)
}

// XOREncrypterOpts represents XOR mode.
type XOREncrypterOpts struct{}

func (opts *XOREncrypterOpts) GetEncryptType() encryptType {
	return ENC_TYPE_XOR
}

func (opts *XOREncrypterOpts) GetKeySize(plaintext []byte) int {
	return len(plaintext)
}

func (opts *XOREncrypterOpts) Encrypt(rand io.Reader, key, plaintext []byte) ([]byte, error) {
	subtle.XORBytes(key, key, plaintext)
	return key, nil
}

func (opts *XOREncrypterOpts) Decrypt(key, ciphertext []byte) ([]byte, error) {
	if len(ciphertext) == 0 {
		return nil, ErrDecryption
	}
	subtle.XORBytes(key, ciphertext, key)
	return key, nil
}

type newCipher func(key []byte) (cipher.Block, error)

type baseBlockEncrypterOpts struct {
	encryptType   encryptType
	newCipher     newCipher
	cipherKeySize int
}

func (opts *baseBlockEncrypterOpts) GetEncryptType() encryptType {
	return opts.encryptType
}

func (opts *baseBlockEncrypterOpts) GetKeySize(plaintext []byte) int {
	return opts.cipherKeySize
}

// CBCEncrypterOpts represents CBC (Cipher block chaining) mode.
type CBCEncrypterOpts struct {
	baseBlockEncrypterOpts
	padding padding.Padding
}

func NewCBCEncrypterOpts(padding padding.Padding, newCipher newCipher, keySize int) EncrypterOpts {
	opts := new(CBCEncrypterOpts)
	opts.encryptType = ENC_TYPE_CBC
	opts.padding = padding
	opts.newCipher = newCipher
	opts.cipherKeySize = keySize
	return opts
}

// Encrypt encrypts the plaintext with the key, includes generated IV at the beginning of the ciphertext.
func (opts *CBCEncrypterOpts) Encrypt(rand io.Reader, key, plaintext []byte) ([]byte, error) {
	block, err := opts.newCipher(key)
	if err != nil {
		return nil, err
	}
	paddedPlainText := opts.padding.Pad(plaintext)
	blockSize := block.BlockSize()
	ciphertext := make([]byte, blockSize+len(paddedPlainText))
	iv := ciphertext[:blockSize]
	if _, err := io.ReadFull(rand, iv); err != nil {
		return nil, err
	}
	mode := cipher.NewCBCEncrypter(block, iv)
	mode.CryptBlocks(ciphertext[blockSize:], paddedPlainText)
	return ciphertext, nil
}

func (opts *CBCEncrypterOpts) Decrypt(key, ciphertext []byte) ([]byte, error) {
	block, err := opts.newCipher(key)
	if err != nil {
		return nil, err
	}
	blockSize := block.BlockSize()
	if len(ciphertext) <= blockSize {
		return nil, ErrDecryption
	}
	iv := ciphertext[:blockSize]
	ciphertext = ciphertext[blockSize:]
	plaintext := make([]byte, len(ciphertext))
	mode := cipher.NewCBCDecrypter(block, iv)
	mode.CryptBlocks(plaintext, ciphertext)
	return opts.padding.Unpad(plaintext)
}

// ECBEncrypterOpts represents ECB (Electronic Code Book) mode.
type ECBEncrypterOpts struct {
	baseBlockEncrypterOpts
	padding padding.Padding
}

func NewECBEncrypterOpts(padding padding.Padding, newCipher newCipher, keySize int) EncrypterOpts {
	opts := new(ECBEncrypterOpts)
	opts.encryptType = ENC_TYPE_ECB
	opts.padding = padding
	opts.newCipher = newCipher
	opts.cipherKeySize = keySize
	return opts
}

func (opts *ECBEncrypterOpts) Encrypt(rand io.Reader, key, plaintext []byte) ([]byte, error) {
	block, err := opts.newCipher(key)
	if err != nil {
		return nil, err
	}
	paddedPlainText := opts.padding.Pad(plaintext)
	ciphertext := make([]byte, len(paddedPlainText))
	mode := _cipher.NewECBEncrypter(block)
	mode.CryptBlocks(ciphertext, paddedPlainText)
	return ciphertext, nil
}

func (opts *ECBEncrypterOpts) Decrypt(key, ciphertext []byte) ([]byte, error) {
	block, err := opts.newCipher(key)
	if err != nil {
		return nil, err
	}
	if len(ciphertext) == 0 {
		return nil, ErrDecryption
	}
	plaintext := make([]byte, len(ciphertext))
	mode := _cipher.NewECBDecrypter(block)
	mode.CryptBlocks(plaintext, ciphertext)
	return opts.padding.Unpad(plaintext)
}

// CFBEncrypterOpts represents CFB (Cipher Feedback) mode.
type CFBEncrypterOpts struct {
	baseBlockEncrypterOpts
}

func NewCFBEncrypterOpts(newCipher newCipher, keySize int) EncrypterOpts {
	opts := new(CFBEncrypterOpts)
	opts.encryptType = ENC_TYPE_CFB
	opts.newCipher = newCipher
	opts.cipherKeySize = keySize
	return opts
}

// Encrypt encrypts the plaintext with the key, includes generated IV at the beginning of the ciphertext.
func (opts *CFBEncrypterOpts) Encrypt(rand io.Reader, key, plaintext []byte) ([]byte, error) {
	block, err := opts.newCipher(key)
	if err != nil {
		return nil, err
	}
	blockSize := block.BlockSize()
	ciphertext := make([]byte, blockSize+len(plaintext))
	iv := ciphertext[:blockSize]
	if _, err := io.ReadFull(rand, iv); err != nil {
		return nil, err
	}
	stream := cipher.NewCFBEncrypter(block, iv)
	stream.XORKeyStream(ciphertext[blockSize:], plaintext)
	return ciphertext, nil
}

func (opts *CFBEncrypterOpts) Decrypt(key, ciphertext []byte) ([]byte, error) {
	block, err := opts.newCipher(key)
	if err != nil {
		return nil, err
	}
	blockSize := block.BlockSize()
	if len(ciphertext) <= blockSize {
		return nil, ErrDecryption
	}
	iv := ciphertext[:blockSize]
	ciphertext = ciphertext[blockSize:]
	plaintext := make([]byte, len(ciphertext))
	stream := cipher.NewCFBDecrypter(block, iv)
	stream.XORKeyStream(plaintext, ciphertext)
	return plaintext, nil
}

// OFBEncrypterOpts represents OFB (Output Feedback) mode.
type OFBEncrypterOpts struct {
	baseBlockEncrypterOpts
}

func NewOFBEncrypterOpts(newCipher newCipher, keySize int) EncrypterOpts {
	opts := new(OFBEncrypterOpts)
	opts.encryptType = ENC_TYPE_OFB
	opts.newCipher = newCipher
	opts.cipherKeySize = keySize
	return opts
}

// Encrypt encrypts the plaintext with the key, includes generated IV at the beginning of the ciphertext.
func (opts *OFBEncrypterOpts) Encrypt(rand io.Reader, key, plaintext []byte) ([]byte, error) {
	block, err := opts.newCipher(key)
	if err != nil {
		return nil, err
	}
	blockSize := block.BlockSize()
	ciphertext := make([]byte, blockSize+len(plaintext))
	iv := ciphertext[:blockSize]
	if _, err := io.ReadFull(rand, iv); err != nil {
		return nil, err
	}
	stream := cipher.NewOFB(block, iv)
	stream.XORKeyStream(ciphertext[blockSize:], plaintext)
	return ciphertext, nil
}

func (opts *OFBEncrypterOpts) Decrypt(key, ciphertext []byte) ([]byte, error) {
	block, err := opts.newCipher(key)
	if err != nil {
		return nil, err
	}
	blockSize := block.BlockSize()
	if len(ciphertext) <= blockSize {
		return nil, ErrDecryption
	}
	iv := ciphertext[:blockSize]
	ciphertext = ciphertext[blockSize:]
	plaintext := make([]byte, len(ciphertext))
	stream := cipher.NewOFB(block, iv)
	stream.XORKeyStream(plaintext, ciphertext)
	return plaintext, nil
}

// DefaultEncrypterOpts default option represents XOR mode
var DefaultEncrypterOpts = new(XOREncrypterOpts)

// SM4ECBEncrypterOpts option represents SM4 ECB mode
var SM4ECBEncrypterOpts = NewECBEncrypterOpts(padding.NewPKCS7Padding(sm4.BlockSize), sm4.NewCipher, sm4.BlockSize)

// SM4CBCEncrypterOpts option represents SM4 CBC mode
var SM4CBCEncrypterOpts = NewCBCEncrypterOpts(padding.NewPKCS7Padding(sm4.BlockSize), sm4.NewCipher, sm4.BlockSize)

// SM4CFBEncrypterOpts option represents SM4 CFB mode
var SM4CFBEncrypterOpts = NewCFBEncrypterOpts(sm4.NewCipher, sm4.BlockSize)

// SM4OFBEncrypterOpts option represents SM4 OFB mode
var SM4OFBEncrypterOpts = NewOFBEncrypterOpts(sm4.NewCipher, sm4.BlockSize)

func shangMiEncrypterOpts(encType encryptType) EncrypterOpts {
	switch encType {
	case ENC_TYPE_XOR:
		return DefaultEncrypterOpts
	case ENC_TYPE_CBC:
		return SM4CBCEncrypterOpts
	case ENC_TYPE_ECB:
		return SM4ECBEncrypterOpts
	case ENC_TYPE_CFB:
		return SM4CFBEncrypterOpts
	case ENC_TYPE_OFB:
		return SM4OFBEncrypterOpts
	}
	return nil
}
