package pkcs

import (
	"crypto/cipher"
	"crypto/des"
	"crypto/md5"
	"crypto/sha1"
	"crypto/x509/pkix"
	"encoding/asn1"
	"errors"
	"hash"
	"io"

	"github.com/emmansun/gmsm/pkcs/internal/md2"
	"github.com/emmansun/gmsm/pkcs/internal/rc2"
)

var (
	pbeWithMD2AndDESCBC  = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 1}
	pbeWithMD2AndRC2CBC  = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 4}
	pbeWithMD5AndDESCBC  = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 3}
	pbeWithMD5AndRC2CBC  = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 6}
	pbeWithSHA1AndDESCBC = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 10}
	pbeWithSHA1AndRC2CBC = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 5, 11}
)

type pbeParameter struct {
	Salt      []byte
	Iteration int
}

// PBES1 implements the Password-Based Encryption Scheme 1.
type PBES1 struct {
	Algorithm pkix.AlgorithmIdentifier
}

// newPBES1 creates a new PBES1 instance.
func newPBES1(rand io.Reader, oid asn1.ObjectIdentifier, saltLen, iterations int) (*PBES1, error) {
	salt := make([]byte, saltLen)
	if _, err := rand.Read(salt); err != nil {
		return nil, err
	}
	param := pbeParameter{Salt: salt, Iteration: iterations}
	marshalledParams, err := asn1.Marshal(param)
	if err != nil {
		return nil, err
	}
	return &PBES1{
		Algorithm: pkix.AlgorithmIdentifier{
			Algorithm:  oid,
			Parameters: asn1.RawValue{FullBytes: marshalledParams},
		},
	}, nil
}

func NewPbeWithMD2AndDESCBC(rand io.Reader, saltLen, iterations int) (*PBES1, error) {
	return newPBES1(rand, pbeWithMD2AndDESCBC, saltLen, iterations)
}

func NewPbeWithMD2AndRC2CBC(rand io.Reader, saltLen, iterations int) (*PBES1, error) {
	return newPBES1(rand, pbeWithMD2AndRC2CBC, saltLen, iterations)
}

func NewPbeWithMD5AndDESCBC(rand io.Reader, saltLen, iterations int) (*PBES1, error) {
	return newPBES1(rand, pbeWithMD5AndDESCBC, saltLen, iterations)
}

func NewPbeWithMD5AndRC2CBC(rand io.Reader, saltLen, iterations int) (*PBES1, error) {
	return newPBES1(rand, pbeWithMD5AndRC2CBC, saltLen, iterations)
}

func NewPbeWithSHA1AndDESCBC(rand io.Reader, saltLen, iterations int) (*PBES1, error) {
	return newPBES1(rand, pbeWithSHA1AndDESCBC, saltLen, iterations)
}

func NewPbeWithSHA1AndRC2CBC(rand io.Reader, saltLen, iterations int) (*PBES1, error) {
	return newPBES1(rand, pbeWithSHA1AndRC2CBC, saltLen, iterations)
}

// Key returns the key derived from the password according PBKDF1.
func (pbes1 *PBES1) key(password []byte) ([]byte, error) {
	param := new(pbeParameter)
	if _, err := asn1.Unmarshal(pbes1.Algorithm.Parameters.FullBytes, param); err != nil {
		return nil, err
	}
	var hash hash.Hash
	switch {
	case pbes1.Algorithm.Algorithm.Equal(pbeWithMD2AndDESCBC) || pbes1.Algorithm.Algorithm.Equal(pbeWithMD2AndRC2CBC):
		hash = md2.New()
	case pbes1.Algorithm.Algorithm.Equal(pbeWithMD5AndDESCBC) || pbes1.Algorithm.Algorithm.Equal(pbeWithMD5AndRC2CBC):
		hash = md5.New()
	case pbes1.Algorithm.Algorithm.Equal(pbeWithSHA1AndDESCBC) || pbes1.Algorithm.Algorithm.Equal(pbeWithSHA1AndRC2CBC):
		hash = sha1.New()
	default:
		return nil, errors.New("pbes: unsupported pbes1 cipher")
	}
	hash.Write(password)
	hash.Write(param.Salt)
	key := hash.Sum(nil)
	for i := 1; i < param.Iteration; i++ {
		hash.Reset()
		hash.Write(key)
		key = hash.Sum(key[:0])
	}
	return key, nil
}

func (pbes1 *PBES1) newBlock(key []byte) (cipher.Block, error) {
	var block cipher.Block
	switch {
	case pbes1.Algorithm.Algorithm.Equal(pbeWithMD2AndDESCBC) ||
		pbes1.Algorithm.Algorithm.Equal(pbeWithMD5AndDESCBC) ||
		pbes1.Algorithm.Algorithm.Equal(pbeWithSHA1AndDESCBC):
		block, _ = des.NewCipher(key[:8])
	case pbes1.Algorithm.Algorithm.Equal(pbeWithMD2AndRC2CBC) ||
		pbes1.Algorithm.Algorithm.Equal(pbeWithMD5AndRC2CBC) ||
		pbes1.Algorithm.Algorithm.Equal(pbeWithSHA1AndRC2CBC):
		block, _ = rc2.NewCipher(key[:8])
	default:
		return nil, errors.New("pbes: unsupported pbes1 cipher")
	}
	return block, nil
}

func (pbes1 *PBES1) Encrypt(rand io.Reader, password, plaintext []byte) (*pkix.AlgorithmIdentifier, []byte, error) {
	key, err := pbes1.key(password)
	if err != nil {
		return nil, nil, err
	}
	block, err := pbes1.newBlock(key)
	if err != nil {
		return nil, nil, err
	}
	ciphertext, err := cbcEncrypt(block, key[8:16], plaintext)
	if err != nil {
		return nil, nil, err
	}
	return &pbes1.Algorithm, ciphertext, nil
}

func (pbes1 *PBES1) Decrypt(password, ciphertext []byte) ([]byte, KDFParameters, error) {
	key, err := pbes1.key(password)
	if err != nil {
		return nil, nil, err
	}
	block, err := pbes1.newBlock(key)
	if err != nil {
		return nil, nil, err
	}
	plaintext, err := cbcDecrypt(block, key[8:16], ciphertext)
	if err != nil {
		return nil, nil, ErrPBEDecryption
	}
	return plaintext, nil, nil
}

func IsPBES1(algorithm pkix.AlgorithmIdentifier) bool {
	return algorithm.Algorithm.Equal(pbeWithMD2AndDESCBC) ||
		algorithm.Algorithm.Equal(pbeWithMD2AndRC2CBC) ||
		algorithm.Algorithm.Equal(pbeWithMD5AndDESCBC) ||
		algorithm.Algorithm.Equal(pbeWithMD5AndRC2CBC) ||
		algorithm.Algorithm.Equal(pbeWithSHA1AndDESCBC) ||
		algorithm.Algorithm.Equal(pbeWithSHA1AndRC2CBC)
}
