// BSD 3-Clause License
//
// Copyright (c) 2019, Guillaume Ballet
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice, this
//   list of conditions and the following disclaimer.
//
// * 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.
//
// * Neither the name of the copyright holder nor the names of its
//   contributors may be used to endorse or promote products derived from
//   this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER OR CONTRIBUTORS 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.

package pcsc

import (
	"encoding/binary"
	"fmt"
	"net"
	"sync"
	"unsafe"
)

// Client contains all the information needed to establish
// and maintain a connection to the deamon/card.
type Client struct {
	conn net.Conn

	minor uint32
	major uint32

	ctx uint32

	mutex sync.Mutex

	readerStateDescriptors [MaxReaderStateDescriptors]ReaderState
}

// EstablishContext asks the PCSC daemon to create a context
// handle for further communication with connected cards and
// readers.
func EstablishContext(path string, scope uint32) (*Client, error) {
	client := &Client{}

	conn, err := clientSetupSession(path)
	if err != nil {
		return nil, err
	}
	client.conn = conn

	payload := make([]byte, 12)
	response := make([]byte, 12)

	var code uint32
	var minor uint32
	for minor = ProtocolVersionMinor; minor <= ProtocolVersionMinor+1; minor++ {
		/* Exchange version information */
		binary.LittleEndian.PutUint32(payload, ProtocolVersionMajor)
		binary.LittleEndian.PutUint32(payload[4:], minor)
		binary.LittleEndian.PutUint32(payload[8:], SCardSuccess.Code())
		err = messageSendWithHeader(CommandVersion, conn, payload)
		if err != nil {
			return nil, err
		}
		n, err := conn.Read(response)
		if err != nil {
			return nil, err
		}
		if n != len(response) {
			return nil, fmt.Errorf("invalid response length: expected %d, got %d", len(response), n)
		}
		code = binary.LittleEndian.Uint32(response[8:])
		if code != SCardSuccess.Code() {
			continue
		}
		client.major = binary.LittleEndian.Uint32(response)
		client.minor = binary.LittleEndian.Uint32(response[4:])
		if client.major != ProtocolVersionMajor || client.minor != minor {
			continue
		}
		break
	}

	if code != SCardSuccess.Code() {
		return nil, fmt.Errorf("invalid response code: expected %d, got %d (%v)", SCardSuccess, code, ErrorCode(code).Error())
	}
	if client.major != ProtocolVersionMajor || (client.minor != minor && client.minor+1 != minor) {
		return nil, fmt.Errorf("invalid version found: expected %d.%d, got %d.%d", ProtocolVersionMajor, ProtocolVersionMinor, client.major, client.minor)
	}

	/* Establish the context proper */
	binary.LittleEndian.PutUint32(payload, scope)
	binary.LittleEndian.PutUint32(payload[4:], 0)
	binary.LittleEndian.PutUint32(payload[8:], SCardSuccess.Code())
	err = messageSendWithHeader(SCardEstablishContext, conn, payload)
	if err != nil {
		return nil, err
	}
	response = make([]byte, 12)
	n, err := conn.Read(response)
	if err != nil {
		return nil, err
	}
	if n != len(response) {
		return nil, fmt.Errorf("invalid response length: expected %d, got %d", len(response), n)
	}
	code = binary.LittleEndian.Uint32(response[8:])
	if code != SCardSuccess.Code() {
		return nil, fmt.Errorf("invalid response code: expected %d, got %d (%v)", SCardSuccess, code, ErrorCode(code).Error())
	}
	client.ctx = binary.LittleEndian.Uint32(response[4:])

	return client, nil
}

// ReleaseContext tells the daemon that the client will no longer
// need the context.
func (client *Client) ReleaseContext() error {
	client.mutex.Lock()
	defer client.mutex.Unlock()

	data := [8]byte{}
	binary.LittleEndian.PutUint32(data[:], client.ctx)
	binary.LittleEndian.PutUint32(data[4:], SCardSuccess.Code())
	err := messageSendWithHeader(SCardReleaseContext, client.conn, data[:])
	if err != nil {
		return err
	}
	total := 0
	for total < len(data) {
		n, err := client.conn.Read(data[total:])
		if err != nil {
			return err
		}
		total += n
	}
	code := binary.LittleEndian.Uint32(data[4:])
	if code != SCardSuccess.Code() {
		return fmt.Errorf("invalid return code: %x, %v", code, ErrorCode(code).Error())
	}

	return nil
}

// Constants related to the reader state structure
const (
	ReaderStateNameLength       = 128
	ReaderStateMaxAtrSizeLength = 33
	// NOTE: ATR is 32-byte aligned in the C version, which means it's
	// actually 36 byte long and not 33.
	ReaderStateDescriptorLength = ReaderStateNameLength + ReaderStateMaxAtrSizeLength + 5*4 + 3

	MaxReaderStateDescriptors = 16
)

// ReaderState represent the state of a single reader, as reported
// by the PCSC daemon.
type ReaderState struct {
	Name          string /* reader name */
	eventCounter  uint32 /* number of card events */
	readerState   uint32 /* SCARD_* bit field */
	readerSharing uint32 /* PCSCLITE_SHARING_* sharing status */

	cardAtr       [ReaderStateMaxAtrSizeLength]byte /* ATR */
	cardAtrLength uint32                            /* ATR length */
	cardProtocol  uint32                            /* SCARD_PROTOCOL_* value */
}

func getReaderState(data []byte) (ReaderState, error) {
	ret := ReaderState{}
	if len(data) < ReaderStateDescriptorLength {
		return ret, fmt.Errorf("could not unmarshall data of length %d < %d", len(data), ReaderStateDescriptorLength)
	}

	ret.Name = string(data[:ReaderStateNameLength])
	ret.eventCounter = binary.LittleEndian.Uint32(data[unsafe.Offsetof(ret.eventCounter):])
	ret.readerState = binary.LittleEndian.Uint32(data[unsafe.Offsetof(ret.readerState):])
	ret.readerSharing = binary.LittleEndian.Uint32(data[unsafe.Offsetof(ret.readerSharing):])
	copy(ret.cardAtr[:], data[unsafe.Offsetof(ret.cardAtr):unsafe.Offsetof(ret.cardAtr)+ReaderStateMaxAtrSizeLength])
	ret.cardAtrLength = binary.LittleEndian.Uint32(data[unsafe.Offsetof(ret.cardAtrLength):])
	ret.cardProtocol = binary.LittleEndian.Uint32(data[unsafe.Offsetof(ret.cardProtocol):])

	return ret, nil
}

// ListReaders gets the list of readers from the daemon
func (client *Client) ListReaders() ([]string, error) {
	client.mutex.Lock()
	defer client.mutex.Unlock()

	err := messageSendWithHeader(CommandGetReaderState, client.conn, []byte{})
	if err != nil {
		return nil, err
	}
	response := make([]byte, ReaderStateDescriptorLength*MaxReaderStateDescriptors)
	total := 0
	for total < len(response) {
		n, err := client.conn.Read(response[total:])
		if err != nil {
			return nil, err
		}
		total += n
	}

	var names []string
	for i := range client.readerStateDescriptors {
		desc, err := getReaderState(response[i*ReaderStateDescriptorLength:])
		if err != nil {
			return nil, err
		}
		client.readerStateDescriptors[i] = desc
		if desc.Name[0] == 0 {
			break
		}
		names = append(names, desc.Name)
	}

	return names, nil
}

// Offsets into the Connect request/response packet
const (
	SCardConnectReaderNameOffset        = 4
	SCardConnectShareModeOffset         = SCardConnectReaderNameOffset + ReaderStateNameLength
	SCardConnectPreferredProtocolOffset = SCardConnectShareModeOffset + 4
	SCardConnectReturnValueOffset       = SCardConnectPreferredProtocolOffset + 12
)

// Card represents the connection to a card
type Card struct {
	handle      uint32
	activeProto uint32
	client      *Client
}

// Connect asks the daemon to connect to the card
func (client *Client) Connect(name string, shareMode uint32, preferredProtocol uint32) (*Card, error) {
	client.mutex.Lock()
	defer client.mutex.Unlock()

	request := make([]byte, ReaderStateNameLength+4*6)
	binary.LittleEndian.PutUint32(request, client.ctx)
	copy(request[SCardConnectReaderNameOffset:], []byte(name))
	binary.LittleEndian.PutUint32(request[SCardConnectShareModeOffset:], shareMode)
	binary.LittleEndian.PutUint32(request[SCardConnectPreferredProtocolOffset:], preferredProtocol)
	binary.LittleEndian.PutUint32(request[SCardConnectReturnValueOffset:], SCardSuccess.Code())

	err := messageSendWithHeader(SCardConnect, client.conn, request)
	if err != nil {
		return nil, err
	}
	response := make([]byte, ReaderStateNameLength+4*6)
	total := 0
	for total < len(response) {
		n, err := client.conn.Read(response[total:])
		if err != nil {
			return nil, err
		}
		// fmt.Println("total, n", total, n, response)
		total += n
	}
	code := binary.LittleEndian.Uint32(response[148:])
	if code != SCardSuccess.Code() {
		return nil, fmt.Errorf("invalid return code: %x (%v)", code, ErrorCode(code).Error())
	}
	handle := binary.LittleEndian.Uint32(response[140:])
	active := binary.LittleEndian.Uint32(response[SCardConnectPreferredProtocolOffset:])

	return &Card{handle: handle, activeProto: active, client: client}, nil
}

/**
* @brief contained in \ref SCARD_TRANSMIT Messages.
*
* These data are passed throw the field \c sharedSegmentMsg.data.
 */
//type transmit struct {
//hCard             uint32
//ioSendPciProtocol uint32
//ioSendPciLength   uint32
//cbSendLength      uint32
//ioRecvPciProtocol uint32
//ioRecvPciLength   uint32
//pcbRecvLength     uint32
//rv                uint32
//}

// SCardIoRequest contains the info needed for performing an IO request
type SCardIoRequest struct {
	proto  uint32
	length uint32
}

const (
	TransmitRequestLength = 32
)

// Transmit sends request data to a card and returns the response
func (card *Card) Transmit(adpu []byte) ([]byte, *SCardIoRequest, error) {
	card.client.mutex.Lock()
	defer card.client.mutex.Unlock()

	request := [TransmitRequestLength]byte{}
	binary.LittleEndian.PutUint32(request[:], card.handle)
	binary.LittleEndian.PutUint32(request[4:] /*card.activeProto*/, 2)
	binary.LittleEndian.PutUint32(request[8:], 8)
	binary.LittleEndian.PutUint32(request[12:], uint32(len(adpu)))
	binary.LittleEndian.PutUint32(request[16:], 0)
	binary.LittleEndian.PutUint32(request[20:], 0)
	binary.LittleEndian.PutUint32(request[24:], 0x10000)
	binary.LittleEndian.PutUint32(request[28:], SCardSuccess.Code())
	err := messageSendWithHeader(SCardTransmit, card.client.conn, request[:])
	if err != nil {
		return nil, nil, err
	}
	// Add the ADPU payload after the transmit descriptor
	n, err := card.client.conn.Write(adpu)
	if err != nil {
		return nil, nil, err
	}
	if n != len(adpu) {
		return nil, nil, fmt.Errorf("invalid number of bytes written: expected %d, got %d", len(adpu), n)
	}
	response := [TransmitRequestLength]byte{}
	total := 0
	for total < len(response) {
		n, err = card.client.conn.Read(response[total:])
		if err != nil {
			return nil, nil, err
		}
		total += n
	}

	code := binary.LittleEndian.Uint32(response[28:])
	if code != SCardSuccess.Code() {
		return nil, nil, fmt.Errorf("invalid return code: %x (%v)", code, ErrorCode(code).Error())
	}

	// Recover the response data
	recvProto := binary.LittleEndian.Uint32(response[16:])
	recvLength := binary.LittleEndian.Uint32(response[20:])
	recv := &SCardIoRequest{proto: recvProto, length: recvLength}
	recvLength = binary.LittleEndian.Uint32(response[24:])
	recvData := make([]byte, recvLength)
	total = 0
	for uint32(total) < recvLength {
		n, err := card.client.conn.Read(recvData[total:])
		if err != nil {
			return nil, nil, err
		}
		total += n
	}

	return recvData, recv, nil
}

// Disconnect tells the PCSC daemon that the client is no longer
// interested in communicating with the card.
func (card *Card) Disconnect(disposition uint32) error {
	card.client.mutex.Lock()
	defer card.client.mutex.Unlock()

	data := [12]byte{}
	binary.LittleEndian.PutUint32(data[:], card.handle)
	binary.LittleEndian.PutUint32(data[4:], disposition)
	binary.LittleEndian.PutUint32(data[8:], SCardSuccess.Code())
	err := messageSendWithHeader(SCardDisConnect, card.client.conn, data[:])
	if err != nil {
		return err
	}
	total := 0
	for total < len(data) {
		n, err := card.client.conn.Read(data[total:])
		if err != nil {
			return err
		}
		total += n
	}
	code := binary.LittleEndian.Uint32(data[8:])
	if code != SCardSuccess.Code() {
		return fmt.Errorf("invalid return code: %x (%v)", code, ErrorCode(code).Error())
	}

	return nil
}