package client

import (
	"errors"
	"io"
	"strings"
	"sync"
	"time"

	"github.com/yosssi/gmq/mqtt"
	"github.com/yosssi/gmq/mqtt/packet"
)

// Minimum and maximum Packet Identifiers
const (
	minPacketID uint16 = 1
	maxPacketID uint16 = 65535
)

// Error values
var (
	ErrAlreadyConnected = errors.New("the Client has already connected to the Server")
	ErrNotYetConnected  = errors.New("the Client has not yet connected to the Server")
	ErrCONNACKTimeout   = errors.New("the CONNACK Packet was not received within a reasonalbe amount of time")
	ErrPINGRESPTimeout  = errors.New("the PINGRESP Packet was not received within a reasonalbe amount of time")
	ErrPacketIDExhaused = errors.New("Packet Identifiers are exhausted")
	ErrInvalidPINGRESP  = errors.New("invalid PINGRESP Packet")
	ErrInvalidSUBACK    = errors.New("invalid SUBACK Packet")
)

// Client represents a Client.
type Client struct {
	// muConn is the Mutex for the Network Connection.
	muConn sync.RWMutex
	// conn is the Network Connection.
	conn *connection

	// muSess is the Mutex for the Session.
	muSess sync.RWMutex
	// sess is the Session.
	sess *session

	// wg is the Wait Group for the goroutines
	// which are launched by the New method.
	wg sync.WaitGroup
	// disconnc is the channel which handles the signal
	// to disconnect the Network Connection.
	disconnc chan struct{}
	// disconnEndc is the channel which ends the goroutine
	// which disconnects the Network Connection.
	disconnEndc chan struct{}

	// errorHandler is the error handler.
	errorHandler ErrorHandler
}

// Connect establishes a Network Connection to the Server and
// sends a CONNECT Packet to the Server.
func (cli *Client) Connect(opts *ConnectOptions) error {
	// Lock for the connection.
	cli.muConn.Lock()

	// Unlock.
	defer cli.muConn.Unlock()

	// Return an error if the Client has already connected to the Server.
	if cli.conn != nil {
		return ErrAlreadyConnected
	}

	// Initialize the options.
	if opts == nil {
		opts = &ConnectOptions{}
	}

	// Establish a Network Connection.
	conn, err := newConnection(opts.Network, opts.Address, opts.TLSConfig)
	if err != nil {
		return err
	}

	// Set the Network Connection to the Client.
	cli.conn = conn

	// Lock for reading and updating the Session.
	cli.muSess.Lock()

	// Create a Session or reuse the current Session.
	if opts.CleanSession || cli.sess == nil {
		// Create a Session and set it to the Client.
		cli.sess = newSession(opts.CleanSession, opts.ClientID)
	} else {
		// Reuse the Session and set its Client Identifier to the options.
		opts.ClientID = cli.sess.clientID
	}

	// Unlock.
	cli.muSess.Unlock()

	// Send a CONNECT Packet to the Server.
	err = cli.sendCONNECT(&packet.CONNECTOptions{
		ClientID:     opts.ClientID,
		UserName:     opts.UserName,
		Password:     opts.Password,
		CleanSession: opts.CleanSession,
		KeepAlive:    opts.KeepAlive,
		WillTopic:    opts.WillTopic,
		WillMessage:  opts.WillMessage,
		WillQoS:      opts.WillQoS,
		WillRetain:   opts.WillRetain,
	})

	if err != nil {
		// Close the Network Connection.
		cli.conn.Close()

		// Clean the Network Connection and the Session if necessary.
		cli.clean()

		return err
	}

	// Launch a goroutine which waits for receiving the CONNACK Packet.
	cli.conn.wg.Add(1)
	go cli.waitPacket(cli.conn.connack, opts.CONNACKTimeout, ErrCONNACKTimeout)

	// Launch a goroutine which receives a Packet from the Server.
	cli.conn.wg.Add(1)
	go cli.receivePackets()

	// Launch a goroutine which sends a Packet to the Server.
	cli.conn.wg.Add(1)
	go cli.sendPackets(time.Duration(opts.KeepAlive), opts.PINGRESPTimeout)

	// Resend the unacknowledged PUBLISH and PUBREL Packets to the Server
	// if the Clean Session is false.
	if !opts.CleanSession {
		// Lock for reading and updating the Session.
		cli.muSess.Lock()

		// Unlock.
		defer cli.muSess.Unlock()

		for id, p := range cli.sess.sendingPackets {
			// Extract the MQTT Control MQTT Control Packet type.
			ptype, err := p.Type()
			if err != nil {
				return err
			}

			switch ptype {
			case packet.TypePUBLISH:
				// Set the DUP flag of the PUBLISH Packet to true.
				p.(*packet.PUBLISH).DUP = true
				// Resend the PUBLISH Packet to the Server.
				cli.conn.send <- p
			case packet.TypePUBREL:
				// Resend the PUBREL Packet to the Server.
				cli.conn.send <- p
			default:
				// Delete the Packet from the Session.
				delete(cli.sess.sendingPackets, id)
			}
		}
	}

	return nil
}

// Disconnect sends a DISCONNECT Packet to the Server and
// closes the Network Connection.
func (cli *Client) Disconnect() error {
	// Lock for the disconnection.
	cli.muConn.Lock()

	// Return an error if the Client has not yet connected to the Server.
	if cli.conn == nil {
		// Unlock.
		cli.muConn.Unlock()

		return ErrNotYetConnected
	}

	// Send a DISCONNECT Packet to the Server.
	// Ignore the error returned by the send method because
	// we proceed to the subsequent disconnecting processing
	// even if the send method returns the error.
	cli.send(packet.NewDISCONNECT())

	// Close the Network Connection.
	if err := cli.conn.Close(); err != nil {
		// Unlock.
		cli.muConn.Unlock()

		return err
	}

	// Change the state of the Network Connection to disconnected.
	cli.conn.disconnected = true

	// Send the end signal to the goroutine via the channels.
	select {
	case cli.conn.sendEnd <- struct{}{}:
	default:
	}

	// Unlock.
	cli.muConn.Unlock()

	// Wait until all goroutines end.
	cli.conn.wg.Wait()

	// Lock for cleaning the Network Connection.
	cli.muConn.Lock()

	// Lock for cleaning the Session.
	cli.muSess.Lock()

	// Clean the Network Connection and the Session.
	cli.clean()

	// Unlock.
	cli.muSess.Unlock()

	// Unlock.
	cli.muConn.Unlock()

	return nil
}

// Publish sends a PUBLISH Packet to the Server.
func (cli *Client) Publish(opts *PublishOptions) error {
	// Lock for reading.
	cli.muConn.RLock()

	// Unlock.
	defer cli.muConn.RUnlock()

	// Check the Network Connection.
	if cli.conn == nil {
		return ErrNotYetConnected
	}

	// Initialize the options.
	if opts == nil {
		opts = &PublishOptions{}
	}

	// Create a PUBLISH Packet.
	p, err := cli.newPUBLISHPacket(opts)
	if err != nil {
		return err
	}

	// Send the Packet to the Server.
	cli.conn.send <- p

	return nil
}

// Subscribe sends a SUBSCRIBE Packet to the Server.
func (cli *Client) Subscribe(opts *SubscribeOptions) error {
	// Lock for reading and updating.
	cli.muConn.Lock()

	// Unlock.
	defer cli.muConn.Unlock()

	// Check the Network Connection.
	if cli.conn == nil {
		return ErrNotYetConnected
	}

	// Check the existence of the options.
	if opts == nil || len(opts.SubReqs) == 0 {
		return packet.ErrInvalidNoSubReq
	}

	// Define a Packet Identifier.
	var packetID uint16

	// Define an error.
	var err error

	// Lock for updating the Session.
	cli.muSess.Lock()

	defer cli.muSess.Unlock()

	// Generate a Packet Identifer.
	if packetID, err = cli.generatePacketID(); err != nil {
		return err
	}

	// Create subscription requests for the SUBSCRIBE Packet.
	var subReqs []*packet.SubReq

	for _, s := range opts.SubReqs {
		subReqs = append(subReqs, &packet.SubReq{
			TopicFilter: s.TopicFilter,
			QoS:         s.QoS,
		})
	}

	// Create a SUBSCRIBE Packet.
	p, err := packet.NewSUBSCRIBE(&packet.SUBSCRIBEOptions{
		PacketID: packetID,
		SubReqs:  subReqs,
	})
	if err != nil {
		return err
	}

	// Set the Packet to the Session.
	cli.sess.sendingPackets[packetID] = p

	// Set the subscription information to
	// the Network Connection.
	for _, s := range opts.SubReqs {
		cli.conn.unackSubs[string(s.TopicFilter)] = s.Handler
	}

	// Send the Packet to the Server.
	cli.conn.send <- p

	return nil
}

// Unsubscribe sends an UNSUBSCRIBE Packet to the Server.
func (cli *Client) Unsubscribe(opts *UnsubscribeOptions) error {
	// Lock for reading and updating.
	cli.muConn.Lock()

	// Unlock.
	defer cli.muConn.Unlock()

	// Check the Network Connection.
	if cli.conn == nil {
		return ErrNotYetConnected
	}

	// Check the existence of the options.
	if opts == nil || len(opts.TopicFilters) == 0 {
		return packet.ErrNoTopicFilter
	}

	// Define a Packet Identifier.
	var packetID uint16

	// Define an error.
	var err error

	// Lock for updating the Session.
	cli.muSess.Lock()

	defer cli.muSess.Unlock()

	// Generate a Packet Identifer.
	if packetID, err = cli.generatePacketID(); err != nil {
		return err
	}

	// Create an UNSUBSCRIBE Packet.
	p, err := packet.NewUNSUBSCRIBE(&packet.UNSUBSCRIBEOptions{
		PacketID:     packetID,
		TopicFilters: opts.TopicFilters,
	})
	if err != nil {
		return err
	}

	// Set the Packet to the Session.
	cli.sess.sendingPackets[packetID] = p

	// Send the Packet to the Server.
	cli.conn.send <- p

	return nil
}

// Terminate ternimates the Client.
func (cli *Client) Terminate() {
	// Send the end signal to the disconnecting goroutine.
	cli.disconnEndc <- struct{}{}

	// Wait until all goroutines end.
	cli.wg.Wait()
}

// send sends an MQTT Control Packet to the Server.
func (cli *Client) send(p packet.Packet) error {
	// Return an error if the Client has not yet connected to the Server.
	if cli.conn == nil {
		return ErrNotYetConnected
	}

	// Write the Packet to the buffered writer.
	if _, err := p.WriteTo(cli.conn.w); err != nil {
		return err
	}

	// Flush the buffered writer.
	return cli.conn.w.Flush()
}

// sendCONNECT creates a CONNECT Packet and sends it to the Server.
func (cli *Client) sendCONNECT(opts *packet.CONNECTOptions) error {
	// Initialize the options.
	if opts == nil {
		opts = &packet.CONNECTOptions{}
	}

	// Create a CONNECT Packet.
	p, err := packet.NewCONNECT(opts)
	if err != nil {
		return err
	}

	// Send a CONNECT Packet to the Server.
	return cli.send(p)
}

// receive receives an MQTT Control Packet from the Server.
func (cli *Client) receive() (packet.Packet, error) {
	// Return an error if the Client has not yet connected to the Server.
	if cli.conn == nil {
		return nil, ErrNotYetConnected
	}

	// Get the first byte of the Packet.
	b, err := cli.conn.r.ReadByte()
	if err != nil {
		return nil, err
	}

	// Create the Fixed header.
	fixedHeader := packet.FixedHeader([]byte{b})

	// Get and decode the Remaining Length.
	var mp uint32 = 1 // multiplier
	var rl uint32     // the Remaining Length
	for {
		// Get the next byte of the Packet.
		b, err = cli.conn.r.ReadByte()
		if err != nil {
			return nil, err
		}

		fixedHeader = append(fixedHeader, b)

		rl += uint32(b&0x7F) * mp

		if b&0x80 == 0 {
			break
		}

		mp *= 128
	}

	// Create the Remaining (the Variable header and the Payload).
	remaining := make([]byte, rl)

	if rl > 0 {
		// Get the remaining of the Packet.
		if _, err = io.ReadFull(cli.conn.r, remaining); err != nil {
			return nil, err
		}
	}

	// Create and return a Packet.
	return packet.NewFromBytes(fixedHeader, remaining)
}

// clean cleans the Network Connection and the Session if necessary.
func (cli *Client) clean() {
	// Clean the Network Connection.
	cli.conn = nil

	// Clean the Session if the Clean Session is true.
	if cli.sess != nil && cli.sess.cleanSession {
		cli.sess = nil
	}
}

// waitPacket waits for receiving the Packet.
func (cli *Client) waitPacket(packetc <-chan struct{}, timeout time.Duration, errTimeout error) {
	defer cli.conn.wg.Done()

	var timeoutc <-chan time.Time

	if timeout > 0 {
		timeoutc = time.After(timeout * time.Second)
	}

	select {
	case <-packetc:
	case <-timeoutc:
		// Handle the timeout error.
		cli.handleErrorAndDisconn(errTimeout)
	}
}

// receivePackets receives Packets from the Server.
func (cli *Client) receivePackets() {
	defer func() {
		// Close the channel which handles a signal which
		// notifies the arrival of the CONNACK Packet.
		close(cli.conn.connack)

		cli.conn.wg.Done()
	}()

	for {
		// Receive a Packet from the Server.
		p, err := cli.receive()
		if err != nil {
			// Handle the error and disconnect
			// the Network Connection.
			cli.handleErrorAndDisconn(err)

			// End the goroutine.
			return
		}

		// Handle the Packet.
		if err := cli.handlePacket(p); err != nil {
			// Handle the error and disconnect
			// the Network Connection.
			cli.handleErrorAndDisconn(err)

			// End the goroutine.
			return
		}
	}
}

// handlePacket handles the Packet.
func (cli *Client) handlePacket(p packet.Packet) error {
	// Get the MQTT Control Packet type.
	ptype, err := p.Type()
	if err != nil {
		return err
	}

	switch ptype {
	case packet.TypeCONNACK:
		cli.handleCONNACK()
		return nil
	case packet.TypePUBLISH:
		return cli.handlePUBLISH(p)
	case packet.TypePUBACK:
		return cli.handlePUBACK(p)
	case packet.TypePUBREC:
		return cli.handlePUBREC(p)
	case packet.TypePUBREL:
		return cli.handlePUBREL(p)
	case packet.TypePUBCOMP:
		return cli.handlePUBCOMP(p)
	case packet.TypeSUBACK:
		return cli.handleSUBACK(p)
	case packet.TypeUNSUBACK:
		return cli.handleUNSUBACK(p)
	case packet.TypePINGRESP:
		return cli.handlePINGRESP()
	default:
		return packet.ErrInvalidPacketType
	}
}

// handleCONNACK handles the CONNACK Packet.
func (cli *Client) handleCONNACK() {
	// Notify the arrival of the CONNACK Packet if possible.
	select {
	case cli.conn.connack <- struct{}{}:
	default:
	}
}

// handlePUBLISH handles the PUBLISH Packet.
func (cli *Client) handlePUBLISH(p packet.Packet) error {
	// Get the PUBLISH Packet.
	publish := p.(*packet.PUBLISH)

	switch publish.QoS {
	case mqtt.QoS0:
		// Lock for reading.
		cli.muConn.RLock()

		// Unlock.
		defer cli.muConn.RUnlock()

		// Handle the Application Message.
		cli.handleMessage(publish.TopicName, publish.Message)

		return nil
	case mqtt.QoS1:
		// Lock for reading.
		cli.muConn.RLock()

		// Unlock.
		defer cli.muConn.RUnlock()

		// Handle the Application Message.
		cli.handleMessage(publish.TopicName, publish.Message)

		// Create a PUBACK Packet.
		puback, err := packet.NewPUBACK(&packet.PUBACKOptions{
			PacketID: publish.PacketID,
		})
		if err != nil {
			return err
		}

		// Send the Packet to the Server.
		cli.conn.send <- puback

		return nil
	default:
		// Lock for update.
		cli.muSess.Lock()

		// Unlock.
		defer cli.muSess.Unlock()

		// Validate the Packet Identifier.
		if _, exist := cli.sess.receivingPackets[publish.PacketID]; exist {
			return packet.ErrInvalidPacketID
		}

		// Set the Packet to the Session.
		cli.sess.receivingPackets[publish.PacketID] = p

		// Create a PUBREC Packet.
		pubrec, err := packet.NewPUBREC(&packet.PUBRECOptions{
			PacketID: publish.PacketID,
		})
		if err != nil {
			return err
		}

		// Send the Packet to the Server.
		cli.conn.send <- pubrec

		return nil
	}
}

// handlePUBACK handles the PUBACK Packet.
func (cli *Client) handlePUBACK(p packet.Packet) error {
	// Lock for update.
	cli.muSess.Lock()

	// Unlock.
	defer cli.muSess.Unlock()

	// Extract the Packet Identifier of the Packet.
	id := p.(*packet.PUBACK).PacketID

	// Validate the Packet Identifier.
	if err := cli.validatePacketID(cli.sess.sendingPackets, id, packet.TypePUBLISH); err != nil {
		return err
	}

	// Delete the PUBLISH Packet from the Session.
	delete(cli.sess.sendingPackets, id)

	return nil
}

// handlePUBREC handles the PUBREC Packet.
func (cli *Client) handlePUBREC(p packet.Packet) error {
	// Lock for update.
	cli.muSess.Lock()

	// Unlock.
	defer cli.muSess.Unlock()

	// Extract the Packet Identifier of the Packet.
	id := p.(*packet.PUBREC).PacketID

	// Validate the Packet Identifier.
	if err := cli.validatePacketID(cli.sess.sendingPackets, id, packet.TypePUBLISH); err != nil {
		return err
	}

	// Create a PUBREL Packet.
	pubrel, err := packet.NewPUBREL(&packet.PUBRELOptions{
		PacketID: id,
	})
	if err != nil {
		return err
	}

	// Set the PUBREL Packet to the Session.
	cli.sess.sendingPackets[id] = pubrel

	// Send the Packet to the Server.
	cli.conn.send <- pubrel

	return nil
}

// handlePUBREL handles the PUBREL Packet.
func (cli *Client) handlePUBREL(p packet.Packet) error {
	// Lock for update.
	cli.muSess.Lock()

	// Unlock.
	defer cli.muSess.Unlock()

	// Extract the Packet Identifier of the Packet.
	id := p.(*packet.PUBREL).PacketID

	// Validate the Packet Identifier.
	if err := cli.validatePacketID(cli.sess.receivingPackets, id, packet.TypePUBLISH); err != nil {
		return err
	}

	// Get the Packet from the Session.
	publish := cli.sess.receivingPackets[id].(*packet.PUBLISH)

	// Lock for reading.
	cli.muConn.RLock()

	// Handle the Application Message.
	cli.handleMessage(publish.TopicName, publish.Message)

	// Unlock.
	cli.muConn.RUnlock()

	// Delete the Packet from the Session
	delete(cli.sess.receivingPackets, id)

	// Create a PUBCOMP Packet.
	pubcomp, err := packet.NewPUBCOMP(&packet.PUBCOMPOptions{
		PacketID: id,
	})
	if err != nil {
		return err
	}

	// Send the Packet to the Server.
	cli.conn.send <- pubcomp

	return nil
}

// handlePUBCOMP handles the PUBCOMP Packet.
func (cli *Client) handlePUBCOMP(p packet.Packet) error {
	// Lock for update.
	cli.muSess.Lock()

	// Unlock.
	defer cli.muSess.Unlock()

	// Extract the Packet Identifier of the Packet.
	id := p.(*packet.PUBCOMP).PacketID

	// Validate the Packet Identifier.
	if err := cli.validatePacketID(cli.sess.sendingPackets, id, packet.TypePUBREL); err != nil {
		return err
	}

	// Delete the PUBREL Packet from the Session.
	delete(cli.sess.sendingPackets, id)

	return nil
}

// handleSUBACK handles the SUBACK Packet.
func (cli *Client) handleSUBACK(p packet.Packet) error {
	// Lock for update.
	cli.muConn.Lock()
	cli.muSess.Lock()

	// Unlock.
	defer cli.muConn.Unlock()
	defer cli.muSess.Unlock()

	// Extract the Packet Identifier of the Packet.
	id := p.(*packet.SUBACK).PacketID

	// Validate the Packet Identifier.
	if err := cli.validatePacketID(cli.sess.sendingPackets, id, packet.TypeSUBSCRIBE); err != nil {
		return err
	}

	// Get the subscription requests of the SUBSCRIBE Packet.
	subreqs := cli.sess.sendingPackets[id].(*packet.SUBSCRIBE).SubReqs

	// Delete the SUBSCRIBE Packet from the Session.
	delete(cli.sess.sendingPackets, id)

	// Get the Return Codes of the SUBACK Packet.
	returnCodes := p.(*packet.SUBACK).ReturnCodes

	// Check the lengths of the Return Codes.
	if len(returnCodes) != len(subreqs) {
		return ErrInvalidSUBACK
	}

	// Set the subscriptions to the Network Connection.
	for i, code := range returnCodes {
		// Skip if the Return Code is failure.
		if code == packet.SUBACKRetFailure {
			continue
		}

		// Get the Topic Filter.
		topicFilter := string(subreqs[i].TopicFilter)

		// Move the subscription information from
		// unackSubs to ackedSubs.
		cli.conn.ackedSubs[topicFilter] = cli.conn.unackSubs[topicFilter]
		delete(cli.conn.unackSubs, topicFilter)
	}

	return nil
}

// handleUNSUBACK handles the UNSUBACK Packet.
func (cli *Client) handleUNSUBACK(p packet.Packet) error {
	// Lock for update.
	cli.muConn.Lock()
	cli.muSess.Lock()

	// Unlock.
	defer cli.muConn.Unlock()
	defer cli.muSess.Unlock()

	// Extract the Packet Identifier of the Packet.
	id := p.(*packet.UNSUBACK).PacketID

	// Validate the Packet Identifier.
	if err := cli.validatePacketID(cli.sess.sendingPackets, id, packet.TypeUNSUBSCRIBE); err != nil {
		return err
	}

	// Get the Topic Filters of the UNSUBSCRIBE Packet.
	topicFilters := cli.sess.sendingPackets[id].(*packet.UNSUBSCRIBE).TopicFilters

	// Delete the UNSUBSCRIBE Packet from the Session.
	delete(cli.sess.sendingPackets, id)

	// Delete the Topic Filters from the Network Connection.
	for _, topicFilter := range topicFilters {
		delete(cli.conn.ackedSubs, string(topicFilter))
	}

	return nil
}

// handlePINGRESP handles the PINGRESP Packet.
func (cli *Client) handlePINGRESP() error {
	// Lock for reading and updating pingrespcs.
	cli.conn.muPINGRESPs.Lock()

	// Check the length of pingrespcs.
	if len(cli.conn.pingresps) == 0 {
		// Unlock.
		cli.conn.muPINGRESPs.Unlock()

		// Return an error if there is no channel in pingrespcs.
		return ErrInvalidPINGRESP
	}

	// Get the first channel in pingrespcs.
	pingrespc := cli.conn.pingresps[0]

	// Remove the first channel from pingrespcs.
	cli.conn.pingresps = cli.conn.pingresps[1:]

	// Unlock.
	cli.conn.muPINGRESPs.Unlock()

	// Notify the arrival of the PINGRESP Packet if possible.
	select {
	case pingrespc <- struct{}{}:
	default:
	}

	return nil
}

// handleError handles the error and disconnects
// the Network Connection.
func (cli *Client) handleErrorAndDisconn(err error) {
	// Lock for reading.
	cli.muConn.RLock()

	// Ignore the error and end the process
	// if the Network Connection has already
	// been disconnected.
	if cli.conn == nil || cli.conn.disconnected {
		// Unlock.
		cli.muConn.RUnlock()

		return
	}

	// Unlock.
	cli.muConn.RUnlock()

	// Handle the error.
	if cli.errorHandler != nil {
		cli.errorHandler(err)
	}

	// Send a disconnect signal to the goroutine
	// via the channel if possible.
	select {
	case cli.disconnc <- struct{}{}:
	default:
	}
}

// sendPackets sends Packets to the Server.
func (cli *Client) sendPackets(keepAlive time.Duration, pingrespTimeout time.Duration) {
	defer func() {
		// Lock for reading and updating pingrespcs.
		cli.conn.muPINGRESPs.Lock()

		// Close the channels which handle a signal which
		// notifies the arrival of the PINGREQ Packet.
		for _, pingresp := range cli.conn.pingresps {
			close(pingresp)
		}

		// Initialize pingrespcs
		cli.conn.pingresps = make([]chan struct{}, 0)

		// Unlock.
		cli.conn.muPINGRESPs.Unlock()

		cli.conn.wg.Done()
	}()

	for {
		var keepAlivec <-chan time.Time

		if keepAlive > 0 {
			keepAlivec = time.After(keepAlive * time.Second)
		}

		select {
		case p := <-cli.conn.send:
			// Lock for sending the Packet.
			cli.muConn.RLock()

			// Send the Packet to the Server.
			err := cli.send(p)

			// Unlock.
			cli.muConn.RUnlock()

			if err != nil {
				// Handle the error and disconnect the Network Connection.
				cli.handleErrorAndDisconn(err)

				// End this function.
				return
			}
		case <-keepAlivec:
			// Lock for sending the Packet.
			cli.muConn.RLock()

			// Send a PINGREQ Packet to the Server.
			err := cli.send(packet.NewPINGREQ())

			// Unlock.
			cli.muConn.RUnlock()

			if err != nil {
				// Handle the error and disconnect the Network Connection.
				cli.handleErrorAndDisconn(err)

				// End this function.
				return
			}

			// Create a channel which handles the signal to notify the arrival of
			// the PINGRESP Packet.
			pingresp := make(chan struct{})

			// Lock for appending the channel to pingrespcs.
			cli.conn.muPINGRESPs.Lock()

			// Append the channel to pingrespcs.
			cli.conn.pingresps = append(cli.conn.pingresps, pingresp)

			// Unlock.
			cli.conn.muPINGRESPs.Unlock()

			// Launch a goroutine which waits for receiving the PINGRESP Packet.
			cli.conn.wg.Add(1)
			go cli.waitPacket(pingresp, pingrespTimeout, ErrPINGRESPTimeout)
		case <-cli.conn.sendEnd:
			// End this function.
			return
		}
	}
}

// generatePacketID generates and returns a Packet Identifier.
func (cli *Client) generatePacketID() (uint16, error) {
	// Define a Packet Identifier.
	id := minPacketID

	for {
		// Find a Packet Identifier which does not used.
		if _, exist := cli.sess.sendingPackets[id]; !exist {
			// Return the Packet Identifier.
			return id, nil
		}

		if id == maxPacketID {
			break
		}

		id++
	}

	// Return an error if available ids are not found.
	return 0, ErrPacketIDExhaused
}

// newPUBLISHPacket creates and returns a PUBLISH Packet.
func (cli *Client) newPUBLISHPacket(opts *PublishOptions) (packet.Packet, error) {
	// Define a Packet Identifier.
	var packetID uint16

	if opts.QoS != mqtt.QoS0 {
		// Lock for reading and updating the Session.
		cli.muSess.Lock()

		defer cli.muSess.Unlock()

		// Define an error.
		var err error

		// Generate a Packet Identifer.
		if packetID, err = cli.generatePacketID(); err != nil {
			return nil, err
		}
	}

	// Create a PUBLISH Packet.
	p, err := packet.NewPUBLISH(&packet.PUBLISHOptions{
		QoS:       opts.QoS,
		Retain:    opts.Retain,
		TopicName: opts.TopicName,
		PacketID:  packetID,
		Message:   opts.Message,
	})
	if err != nil {
		return nil, err
	}

	if opts.QoS != mqtt.QoS0 {
		// Set the Packet to the Session.
		cli.sess.sendingPackets[packetID] = p
	}

	// Return the Packet.
	return p, nil
}

// validateSendingPacketID checks if the Packet which has
// the Packet Identifier and the MQTT Control Packet type
// specified by the parameters exists in the Session's
// sendingPackets.
func (cli *Client) validatePacketID(packets map[uint16]packet.Packet, id uint16, ptype byte) error {
	// Extract the Packet.
	p, exist := packets[id]

	if !exist {
		// Return an error if there is no Packet which has the Packet Identifier
		// specified by the parameter.
		return packet.ErrInvalidPacketID
	}

	// Extract the MQTT Control Packet type of the Packet.
	t, err := p.Type()
	if err != nil {
		return err
	}

	if t != ptype {
		// Return an error if the Packet's MQTT Control Packet type does not
		// equal to one specified by the parameter.
		return packet.ErrInvalidPacketID
	}

	return nil
}

// handleMessage handles the Application Message.
func (cli *Client) handleMessage(topicName, message []byte) {
	// Get the string of the Topic Name.
	topicNameStr := string(topicName)

	for topicFilter, handler := range cli.conn.ackedSubs {
		if handler == nil || !match(topicNameStr, topicFilter) {
			continue
		}

		// Execute the handler.
		go handler(topicName, message)
	}
}

// New creates and returns a Client.
func New(opts *Options) *Client {
	// Initialize the options.
	if opts == nil {
		opts = &Options{}
	}
	// Create a Client.
	cli := &Client{
		disconnc:     make(chan struct{}, 1),
		disconnEndc:  make(chan struct{}),
		errorHandler: opts.ErrorHandler,
	}

	// Launch a goroutine which disconnects the Network Connection.
	cli.wg.Add(1)
	go func() {
		defer func() {
			cli.wg.Done()
		}()

		for {
			select {
			case <-cli.disconnc:
				if err := cli.Disconnect(); err != nil {
					if cli.errorHandler != nil {
						cli.errorHandler(err)
					}
				}
			case <-cli.disconnEndc:
				// End the goroutine.
				return
			}
		}

	}()

	// Return the Client.
	return cli
}

// match checks if the Topic Name matches the Topic Filter.
func match(topicName, topicFilter string) bool {
	// Tokenize the Topic Name.
	nameTokens := strings.Split(topicName, "/")
	nameTokensLen := len(nameTokens)

	// Tolenize the Topic Filter.
	filterTokens := strings.Split(topicFilter, "/")

	for i, t := range filterTokens {
		switch t {
		case "#":
			return i != 0 || !strings.HasPrefix(nameTokens[0], "$")
		case "+":
			if i == 0 && strings.HasPrefix(nameTokens[0], "$") {
				return false
			}

			if nameTokensLen <= i {
				return false
			}
		default:
			if nameTokensLen <= i || t != nameTokens[i] {
				return false
			}
		}
	}

	return len(filterTokens) == nameTokensLen
}
