#!/usr/bin/env python
import os
import sys
from ctypes import (
    POINTER,
    c_char_p,
    c_double,
    c_int,
    c_size_t,
    c_uint32,
    c_uint64,
    cdll,
    create_string_buffer,
    pointer,
)

if sys.version_info >= (3, 8) and sys.platform == 'win32':
    lib_path = os.path.join(os.path.normpath(sys.prefix), 'Library', 'bin')
    build_dir = os.path.join(os.path.dirname(__file__), '../')
    if os.path.exists(lib_path):
        os.add_dll_directory(lib_path)
    if os.path.exists(build_dir):
        os.add_dll_directory(build_dir)
import importlib
import importlib.util

_scrypt = cdll.LoadLibrary(importlib.util.find_spec('_scrypt').origin)

__version__ = '0.8.27'

_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
    c_int,  # int            verbose
]
_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
    c_int,  # int            verbose
    c_int,  # int            force
]
_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


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


def _ensure_bytes(data):
    if 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)
    # verbose is set to zero
    result = _scryptenc_buf(
        input,
        len(input),
        outbuf,
        password,
        len(password),
        maxmem,
        maxmemfrac,
        maxtime,
        0,
    )
    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)
    # verbose and force are set to zero
    result = _scryptdec_buf(
        input,
        len(input),
        outbuf,
        outbuflen,
        password,
        len(password),
        maxmem,
        maxmemfrac,
        maxtime,
        0,
        0,
    )

    if result:
        raise error(result)

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

    if 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.

    - `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, 0
    )

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

    return outbuf.raw


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