#!/usr/bin/env python3

"""
# Copyright (c) 2010 Jean-Luc Giraud (jlgiraud@mac.com)
# All rights reserved.
"""

# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. The name of the author may not be used to endorse or promote products
#    derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


import sys
import threading
import time

from smartcard.Exceptions import NoReadersException
from smartcard.pcsc.PCSCExceptions import (  # ReleaseContextException,
    BaseSCardException,
    EstablishContextException,
    ListReadersException,
)
from smartcard.scard import (  # SCardReleaseContext,
    SCARD_ATTR_DEVICE_FRIENDLY_NAME_A,
    SCARD_LEAVE_CARD,
    SCARD_PCI_T0,
    SCARD_PROTOCOL_ANY,
    SCARD_PROTOCOL_T0,
    SCARD_S_SUCCESS,
    SCARD_SCOPE_USER,
    SCARD_SHARE_SHARED,
    SCardBeginTransaction,
    SCardConnect,
    SCardEndTransaction,
    SCardEstablishContext,
    SCardGetAttrib,
    SCardGetErrorMessage,
    SCardListReaders,
    SCardReconnect,
    SCardStatus,
    SCardTransmit,
)

# Global variable used by test functions to acknowledge
# that they do not get blocked
unblocked = False


def check(testFunctionName, hresult, duration):
    """check"""
    if hresult != SCARD_S_SUCCESS:
        print(f"{testFunctionName} failed: {SCardGetErrorMessage(hresult)}")
        print('Failure for "Sharing violation" are OK for non blocking calls')
    print(f"{testFunctionName} finished after {duration}s")


def SCardReconnectTest(_hcontextTest, hcardTest, _readerName):
    """SCardReconnectTest"""
    global unblocked
    testFunctionName = sys._getframe().f_code.co_name
    print(f"Test thread for {testFunctionName}")
    before = time.time()
    hresult, _dwDisposition = SCardReconnect(
        hcardTest,
        SCARD_SHARE_SHARED,
        SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T0,
        SCARD_LEAVE_CARD,
    )
    check(testFunctionName, hresult, time.time() - before)
    unblocked = True


def SCardGetAttribTest(_hcontextTest, hcardTest, _readerName):
    """SCardGetAttribTest"""
    global unblocked
    testFunctionName = sys._getframe().f_code.co_name
    print(f"Test thread for {testFunctionName}")
    before = time.time()
    hresult, _attrib = SCardGetAttrib(hcardTest, SCARD_ATTR_DEVICE_FRIENDLY_NAME_A)
    check(testFunctionName, hresult, time.time() - before)
    unblocked = True


def SCardTransmitTest(_hcontextTest, hcardTest, _readerName):
    """SCardTransmitTest"""
    global unblocked
    testFunctionName = sys._getframe().f_code.co_name
    print(f"Test thread for {testFunctionName}")
    SELECT = [0xA0, 0xA4, 0x00, 0x00, 0x02]
    DF_TELECOM = [0x7F, 0x10]
    before = time.time()
    hresult, _response = SCardTransmit(hcardTest, SCARD_PCI_T0, SELECT + DF_TELECOM)
    check(testFunctionName, hresult, time.time() - before)
    unblocked = True


def SCardStatusTest(_hcontextTest, hcardTest, _readerName):
    """SCardStatusTest"""
    global unblocked
    testFunctionName = sys._getframe().f_code.co_name
    print(f"Test thread for {testFunctionName}")
    before = time.time()
    hresult, _reader, _state, _protocol, _atr = SCardStatus(hcardTest)
    check(testFunctionName, hresult, time.time() - before)
    unblocked = True


def SCardConnectTest(_hcontextTest, _hcardTest, readerName):
    """SCardConnectTest"""
    global unblocked
    testFunctionName = sys._getframe().f_code.co_name
    print(f"Test thread for {testFunctionName}")
    hresult, hcontext = SCardEstablishContext(SCARD_SCOPE_USER)
    if hresult != SCARD_S_SUCCESS:
        raise EstablishContextException(hresult)

    before = time.time()
    # Connect in SCARD_SHARE_SHARED mode
    hresult, _hcard, _dwActiveProtocol = SCardConnect(
        hcontext, readerName, SCARD_SHARE_SHARED, SCARD_PROTOCOL_ANY
    )
    check(testFunctionName, hresult, time.time() - before)
    unblocked = True


def SCardBeginTransactionTest(_hcontextTest, hcardTest, _readerName):
    """SCardBeginTransactionTest"""
    global unblocked
    testFunctionName = sys._getframe().f_code.co_name
    print(f"Test thread for {testFunctionName}")
    before = time.time()
    hresult = SCardBeginTransaction(hcardTest)
    if hresult != SCARD_S_SUCCESS:
        print(f"{testFunctionName} failed: {SCardGetErrorMessage(hresult)}")
        print('Failure for "Sharing violation" are OK for non blocking calls')
    hresult = SCardEndTransaction(hcardTest, SCARD_LEAVE_CARD)
    check(testFunctionName, hresult, time.time() - before)
    unblocked = True


# def TemplateTest(hcontextTest, hcardTest, readerName):
#    global unblocked
#    testFunctionName = sys._getframe().f_code.co_name
#    print("Test thread for %s" % testFunctionName)
#    before = time.time()
#    hresult, attrib = Template(hcardTest, SCARD_ATTR_DEVICE_FRIENDLY_NAME_A)
#    check(testFunctionName, hresult, time.time() - before)
#    unblocked = True


# Format:
# 'functioname' : [TestFunction, ShouldBlock?]

tests_SnowLeopard = {
    "SCardReconnect": [SCardReconnectTest, False],
    "SCardGetAttrib": [SCardGetAttribTest, False],
    "SCardTransmit": [SCardTransmitTest, False],
    "SCardStatus": [SCardStatusTest, False],
    "SCardConnect": [SCardConnectTest, True],
    "SCardBeginTransaction": [SCardBeginTransactionTest, True],
}

tests_Win7 = {
    "SCardReconnect": [SCardReconnectTest, True],
    "SCardGetAttrib": [SCardGetAttribTest, False],
    "SCardTransmit": [SCardTransmitTest, True],
    "SCardStatus": [SCardStatusTest, True],
    "SCardConnect": [SCardConnectTest, True],
    "SCardBeginTransaction": [SCardBeginTransactionTest, True],
}

tests_Linux = tests_Win7

# Windows should be the reference implementation
tests = tests_Linux


def Connect(index=0):
    """docstring for Connect"""
    hresult, hcontext = SCardEstablishContext(SCARD_SCOPE_USER)
    if hresult != SCARD_S_SUCCESS:
        raise EstablishContextException(hresult)

    hresult, readers = SCardListReaders(hcontext, [])
    if hresult != SCARD_S_SUCCESS:
        raise ListReadersException(hresult)
    print("PC/SC Readers:", readers)
    if len(readers) <= 0:
        raise NoReadersException()
    reader = readers[index]
    print("Using reader:", reader)

    # Connect in SCARD_SHARE_SHARED mode
    hresult, hcard, _dwActiveProtocol = SCardConnect(
        hcontext, reader, SCARD_SHARE_SHARED, SCARD_PROTOCOL_ANY
    )
    if hresult != SCARD_S_SUCCESS:
        print("SCardConnect failed")
        raise BaseSCardException(hresult)
    return hcontext, hcard, reader


def ConnectWithReader(readerName):
    """docstring for Connect"""
    hresult, hcontext = SCardEstablishContext(SCARD_SCOPE_USER)
    if hresult != SCARD_S_SUCCESS:
        raise EstablishContextException(hresult)

    # Connect in SCARD_SHARE_SHARED mode
    hresult, hcard, _dwActiveProtocol = SCardConnect(
        hcontext, readerName, SCARD_SHARE_SHARED, SCARD_PROTOCOL_ANY
    )
    if hresult != SCARD_S_SUCCESS:
        print("SCardConnect failed")
        raise BaseSCardException(hresult)
    return hcontext, hcard


def main():
    """docstring for main"""
    global unblocked

    RED = "\033[0;31m"
    BLUE = "\033[0;34m"
    NORMAL = "\033[00m"

    # if len(sys.argv) < 4:
    #    print(usage)
    #    sys.exit(1)
    # Allow to specify test name
    # Options:
    #   -m : run manually (independent processes)

    index = 0
    if len(sys.argv) > 1:
        index = int(sys.argv[1])
    _hcontext, hcard, readerName = Connect(index)
    # Creating the test handles here:
    # the test thread can't create them as
    # doing it may block on another call
    hcontextTest, hcardTest = ConnectWithReader(readerName)

    for testTarget, testResults in tests.items():
        # Start test function in a new thread
        SCardBeginTransaction(hcard)
        unblocked = False
        print()
        print(f"Testing {testTarget}, expecting", end=" ")
        methodName, shouldBlock = testResults
        if shouldBlock:
            print("blocking")
        else:
            print("non blocking")
        thread = threading.Thread(
            target=methodName, args=(hcontextTest, hcardTest, readerName)
        )
        thread.start()
        # print("Thread started")
        time.sleep(1)
        failed = (unblocked and shouldBlock) or ((not unblocked) and (not shouldBlock))
        SCardEndTransaction(hcard, SCARD_LEAVE_CARD)

        # wait for the thread to finish
        thread.join()

        if failed:
            print(RED + "Test for " + testTarget + " FAILED!" + NORMAL)
        else:
            print(BLUE + "Test for " + testTarget + " succeeded" + NORMAL)


if __name__ == "__main__":
    main()
