import imp
import os
import sys

from ctypes import (cdll,
                    POINTER, pointer,
                    c_char_p,
                    c_size_t, c_double, c_int, c_uint64, c_uint32,
                    create_string_buffer)

_scrypt = cdll.LoadLibrary(imp.find_module('_scrypt')[1])

_scryptenc_buf = _scrypt.exp_scryptenc_buf
_scryptenc_buf.argtypes = [c_char_p,  # const uint_t  *inbuf
                           c_size_t,  # size_t         inbuflen
                           c_char_p,  # uint8_t       *outbuf
                           c_char_p,  # const uint8_t *passwd
                           c_size_t,  # size_t         passwdlen
                           c_size_t,  # size_t         maxmem
                           c_double,  # double         maxmemfrac
                           c_double,  # double         maxtime
                           ]
_scryptenc_buf.restype = c_int

_scryptdec_buf = _scrypt.exp_scryptdec_buf
_scryptdec_buf.argtypes = [c_char_p,           # const uint8_t *inbuf
                           c_size_t,           # size_t         inbuflen
                           c_char_p,           # uint8_t       *outbuf
                           POINTER(c_size_t),  # size_t        *outlen
                           c_char_p,           # const uint8_t *passwd
                           c_size_t,           # size_t         passwdlen
                           c_size_t,           # size_t         maxmem
                           c_double,           # double         maxmemfrac
                           c_double,           # double         maxtime
                           ]
_scryptdec_buf.restype = c_int

_crypto_scrypt = _scrypt.exp_crypto_scrypt
_crypto_scrypt.argtypes = [c_char_p,  # const uint8_t *passwd
                           c_size_t,  # size_t         passwdlen
                           c_char_p,  # const uint8_t *salt
                           c_size_t,  # size_t         saltlen
                           c_uint64,  # uint64_t       N
                           c_uint32,  # uint32_t       r
                           c_uint32,  # uint32_t       p
                           c_char_p,  # uint8_t       *buf
                           c_size_t,  # size_t         buflen
                           ]
_crypto_scrypt.restype = c_int

ERROR_MESSAGES = ['success',
                  'getrlimit or sysctl(hw.usermem) failed',
                  'clock_getres or clock_gettime failed',
                  'error computing derived key',
                  'could not read salt from /dev/urandom',
                  'error in OpenSSL',
                  'malloc failed',
                  'data is not a valid scrypt-encrypted block',
                  'unrecognized scrypt format',
                  'decrypting file would take too much memory',
                  'decrypting file would take too long',
                  'password is incorrect',
                  'error writing output file',
                  'error reading input file']

MAXMEM_DEFAULT = 0
MAXMEMFRAC_DEFAULT = 0.5
MAXTIME_DEFAULT = 300.0
MAXTIME_DEFAULT_ENC = 5.0

IS_PY2 = sys.version_info < (3, 0, 0, 'final', 0)


class error(Exception):
    def __init__(self, scrypt_code):
        if isinstance(scrypt_code, int):
            self._scrypt_code = scrypt_code
            super(error, self).__init__(ERROR_MESSAGES[scrypt_code])
        else:
            self._scrypt_code = -1
            super(error, self).__init__(scrypt_code)


def _ensure_bytes(data):
    if IS_PY2 and isinstance(data, unicode):
        raise TypeError('can not encrypt/decrypt unicode objects')

    if not IS_PY2 and isinstance(data, str):
        return bytes(data, 'utf-8')

    return data


def encrypt(input, password,
            maxtime=MAXTIME_DEFAULT_ENC,
            maxmem=MAXMEM_DEFAULT,
            maxmemfrac=MAXMEMFRAC_DEFAULT):
    """
    Encrypt a string using a password. The resulting data will have len =
    len(input) + 128.

    Notes for Python 2:
      - `input` and `password` must be str instances
      - The result will be a str instance

    Notes for Python 3:
      - `input` and `password` can be both str and bytes. If they are str
        instances, they will be encoded with utf-8
      - The result will be a bytes instance

    Exceptions raised:
      - TypeError on invalid input
      - scrypt.error if encryption failed

    For more information on the `maxtime`, `maxmem`, and `maxmemfrac`
    parameters, see the scrypt documentation.
    """

    input = _ensure_bytes(input)
    password = _ensure_bytes(password)

    outbuf = create_string_buffer(len(input) + 128)
    result = _scryptenc_buf(input, len(input),
                            outbuf,
                            password, len(password),
                            maxmem, maxmemfrac, maxtime)
    if result:
        raise error(result)

    return outbuf.raw


def decrypt(input, password,
            maxtime=MAXTIME_DEFAULT,
            maxmem=MAXMEM_DEFAULT,
            maxmemfrac=MAXMEMFRAC_DEFAULT,
            encoding='utf-8'):
    """
    Decrypt a string using a password.

    Notes for Python 2:
      - `input` and `password` must be str instances
      - The result will be a str instance
      - The encoding parameter is ignored

    Notes for Python 3:
      - `input` and `password` can be both str and bytes. If they are str
        instances, they wil be encoded with utf-8. `input` *should*
        really be a bytes instance, since that's what `encrypt` returns.
      - The result will be a str instance encoded with `encoding`.
        If encoding=None, the result will be a bytes instance.

    Exceptions raised:
      - TypeError on invalid input
      - scrypt.error if decryption failed

    For more information on the `maxtime`, `maxmem`, and `maxmemfrac`
    parameters, see the scrypt documentation.
    """

    outbuf = create_string_buffer(len(input))
    outbuflen = pointer(c_size_t(0))

    input = _ensure_bytes(input)
    password = _ensure_bytes(password)

    result = _scryptdec_buf(input, len(input),
                            outbuf, outbuflen,
                            password, len(password),
                            maxmem, maxmemfrac, maxtime)

    if result:
        raise error(result)

    out_bytes = outbuf.raw[:outbuflen.contents.value]

    if IS_PY2 or encoding is None:
        return out_bytes

    return str(out_bytes, encoding)


def hash(password, salt, N=1 << 14, r=8, p=1, buflen=64):
    """
    Compute scrypt(password, salt, N, r, p, buflen).

    The parameters r, p, and buflen must satisfy r * p < 2^30 and
    buflen <= (2^32 - 1) * 32. The parameter N must be a power of 2
    greater than 1. N, r and p must all be positive.

    Notes for Python 2:
      - `password` and `salt` must be str instances
      - The result will be a str instance

    Notes for Python 3:
      - `password` and `salt` can be both str and bytes. If they are str
        instances, they wil be encoded with utf-8.
      - The result will be a bytes instance

    Exceptions raised:
      - TypeError on invalid input
      - scrypt.error if scrypt failed
    """

    outbuf = create_string_buffer(buflen)

    password = _ensure_bytes(password)
    salt = _ensure_bytes(salt)

    if r * p >= (1 << 30) or N <= 1 or (N & (N - 1)) != 0 or p < 1 or r < 1:
        raise error('hash parameters are wrong (r*p should be < 2**30, and N should be a power of two > 1)')

    result = _crypto_scrypt(password, len(password),
                            salt, len(salt),
                            N, r, p,
                            outbuf, buflen)

    if result:
        raise error('could not compute hash')

    return outbuf.raw


__all__ = ['error', 'encrypt', 'decrypt', 'hash']
