"""
# -*- coding: utf-8 -*-
# ===============================================================================
#
# Copyright (C) 2013/2017 Laurent Labatut / Laurent Champagnac
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
# ===============================================================================
"""

import logging
from _socket import SOL_SOCKET, SOL_TCP, SO_KEEPALIVE, TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT
from _ssl import PROTOCOL_TLSv1
from threading import Lock

import gevent
import socks
from gevent import GreenletExit
from gevent._sslgte279 import SSLSocket
from gevent.queue import Queue
from pysolbase.SolBase import SolBase
from pysolmeters.AtomicInt import AtomicIntSafe

from pysoltcp.tcpbase.ProtocolParserTextDelimited import ProtocolParserTextDelimited
from pysoltcp.tcpbase.TcpSocketManager import TcpSocketManager
from pysoltcp.tcpclient.TcpClientConfig import TcpClientConfig

SolBase.voodoo_init()

logger = logging.getLogger(__name__)


class TcpSimpleClient(TcpSocketManager):
    """
    Tcp simple client
    """

    def __init__(self, tcp_client_config):
        """
        Constructor.
        Option via TcpClientConfig to enable SSL on socket.
        In this case, SSL handshake is to be done manually AFTER connect. Higher level implementation MUST handle this.
        :param tcp_client_config: The tcp client config.
        :return Nothing.
        """

        # Base - we provide two callback :
        # - one for disconnecting ourselves
        # - one to notify socket receive buffer
        TcpSocketManager.__init__(self, self.disconnect, self._on_receive)

        # Check
        if tcp_client_config is None:
            logger.error("TcpSimpleClient : tcp_serverConfig is None")
            raise Exception("TcpSimpleClient : tcp_serverConfig is None")
        elif not isinstance(tcp_client_config, TcpClientConfig):
            logger.error("TcpSimpleClient : tcp_serverConfig is not a TcpServerConfig, class=%s", SolBase.get_classname(tcp_client_config))
            raise Exception("TcpSimpleClient : tcp_serverConfig is not a TcpServerConfig")

        # Default
        self._tcp_client_config = tcp_client_config

        # Greenlets
        self._read_greenlet = None
        self._write_greenlet = None

        # Receive queue
        self._receive_queue = Queue()

        # Receive current buffer
        self._receive_current_buf = None

        # Socket info
        self._local_addr = None
        self._local_port = None
        self._remote_addr = None
        self._remote_port = None
        self._connect_count = AtomicIntSafe()
        self._disconnect_count = AtomicIntSafe()

        # Pool stuff
        self.process_id = None
        self.pool_id = None
        self.dt_alloc = SolBase.datecurrent()
        self.dt_last_acquire = self.dt_alloc
        self.dt_last_release = self.dt_alloc
        self.count_acquire = 0
        self.count_release = 0

        # Pool disconnect notify
        self.on_disconnect_notifypoolcallbacklock = Lock()
        self.on_disconnect_notifypoolcallbackcall = True
        self.on_disconnect_notifypoolcallback = None

    # ================================
    # POOL METHOD
    # ================================

    def notify_acquire(self):
        """
        Notify
        """
        self.dt_last_acquire = SolBase.datecurrent()
        self.count_acquire += 1

    def notify_release(self):
        """
        Notify
        """
        self.dt_last_release = SolBase.datecurrent()
        self.count_release += 1

    # ================================
    # TO STRING OVERWRITE
    # ================================

    def __str__(self):
        """
        To string override
        :return: A string
        :rtype string
        """

        return "c.addr={0}:{1}/{2}:{3}*cc={4}*dc={5}*c.q.recv.size={6}*sock={7}*proxy={8}/{9}:{10}*{11}".format(
            self._local_addr, self._local_port, self._remote_addr, self._remote_port,
            self._connect_count.get(), self._disconnect_count.get(),
            self._receive_queue.qsize(),
            self.current_socket,
            self._tcp_client_config.proxy_enable,
            self._tcp_client_config.proxy_addr,
            self._tcp_client_config.proxy_port,
            TcpSocketManager.__str__(self)
        )

    # =================================
    # CONNECT
    # =================================

    def connect(self):
        """
        Connect to server. Return true upon success.
        If SSL is enable, do NOT forget at higher level to initiate a SSL handshake manually.
        :return True if connected, false otherwise.
        """

        try:
            logger.debug("TcpSimpleClient : connect : starting, target=%s:%s", self._tcp_client_config.target_addr, self._tcp_client_config.target_port)

            # Reset the error flag right now
            self.unset_internal_fatal_error_for_reconnect()

            # Check
            if self.is_connected:
                logger.warn("TcpSimpleClient : connect : already connected, doing nothing, self=%s", self)
                return False

            # Init
            self._connect_count.increment()
            self._dt_created = SolBase.datecurrent()
            self._dt_last_recv = self._dt_created
            self._dt_last_send = self._dt_created

            # ------------------------
            # ALLOC
            # ------------------------
            if self._tcp_client_config.ssl_enable:
                if self._tcp_client_config.proxy_enable:
                    # ------------------------
                    # PROXY ON, SSL ON
                    # ------------------------
                    if self._tcp_client_config.debug_log:
                        logger.info("TcpSimpleClient : Alloc : PROXY ON/SSL ON, self=%s", self)
                    else:
                        logger.debug("TcpSimpleClient : Alloc : PROXY ON/SSL ON, self=%s", self)

                    # Alloc
                    self.current_socket = socks.socksocket()

                    # Proxy
                    self.current_socket.setproxy(socks.PROXY_TYPE_SOCKS5, self._tcp_client_config.proxy_addr, self._tcp_client_config.proxy_port)

                    # SSL Wrap : Inside POST CONNECT
                else:
                    # ------------------------
                    # PROXY OFF, SSL ON
                    # ------------------------
                    if self._tcp_client_config.debug_log:
                        logger.info("TcpSimpleClient : Alloc : PROXY OFF/SSL ON, self=%s", self)
                    else:
                        logger.debug("TcpSimpleClient : Alloc : PROXY OFF/SSL ON, self=%s", self)

                    # Alloc
                    self.current_socket = gevent.socket.socket()

                    # Wrap
                    self.current_socket = SSLSocket(self.current_socket, do_handshake_on_connect=False, ssl_version=PROTOCOL_TLSv1)
            else:
                if self._tcp_client_config.proxy_enable:
                    # ------------------------
                    # PROXY ON, SSL OFF
                    # ------------------------
                    if self._tcp_client_config.debug_log:
                        logger.info("TcpSimpleClient : Alloc : PROXY ON/SSL OFF, self=%s", self)
                    else:
                        logger.debug("TcpSimpleClient : Alloc : PROXY ON/SSL OFF, self=%s", self)

                    # Alloc
                    self.current_socket = socks.socksocket()

                    # Proxy
                    self.current_socket.setproxy(socks.PROXY_TYPE_SOCKS5, self._tcp_client_config.proxy_addr,
                                                 self._tcp_client_config.proxy_port)
                else:
                    # ------------------------
                    # PROXY OFF, SSL OFF
                    # ------------------------
                    if self._tcp_client_config.debug_log:
                        logger.info("TcpSimpleClient : Alloc : PROXY OFF/SSL OFF, self=%s", self)
                    else:
                        logger.debug("TcpSimpleClient : Alloc : PROXY OFF/SSL OFF, self=%s", self)

                    # Alloc
                    self.current_socket = gevent.socket.socket()

            # ------------------------
            # POST ALLOC
            # ------------------------
            if self._tcp_client_config.timeout_ms:
                logger.debug("TcpSimpleClient : connect : setting timeout=%s, self=%s", self._tcp_client_config.timeout_ms, self)
                self.current_socket.settimeout(None)
                logger.debug("TcpSimpleClient : connect : timeout=%s, self=%s", self.current_socket.gettimeout(), self)

            # ------------------------
            # CONNECT
            # ------------------------

            # 2-tuple (host, port)
            address = (self._tcp_client_config.target_addr, self._tcp_client_config.target_port)

            # Connect
            if self._tcp_client_config.debug_log:
                logger.info("TcpSimpleClient : connect : starting, ssl=%s, timeout=%s, self=%s", self._tcp_client_config.ssl_enable, self._tcp_client_config.timeout_ms, self)

            # Go
            logger.debug("TcpSimpleClient : connect : calling connect, self=%s", self)
            self.current_socket.connect(address)
            logger.debug("TcpSimpleClient : connect : now connected, self=%s", self)

            # Store
            self._local_addr = self.current_socket.getsockname()[0]
            self._local_port = self.current_socket.getsockname()[1]
            self._remote_addr = self.current_socket.getpeername()[0]
            self._remote_port = self.current_socket.getpeername()[1]

            # Log
            if self._tcp_client_config.debug_log:
                logger.info("TcpSimpleClient : connected, %s:%s to %s:%s, self=%s", self._local_addr, self._local_port, self._remote_addr, self._remote_port, self)

            # Done
            self.is_connected = True
            self.on_disconnect_notifypoolcallbackcall = True

            # ------------------------
            # POST CONNECT
            # ------------------------
            if self._tcp_client_config.ssl_enable:
                # Switch to SSL now
                if self._tcp_client_config.debug_log:
                    logger.info("PROXY ON/SSL ON, switching to SSL now, self=%s", self)
                else:
                    logger.debug("PROXY ON/SSL ON, switching to SSL now, self=%s", self)

                # Wrap (already connected
                self.current_socket = SSLSocket(self.current_socket, do_handshake_on_connect=False, ssl_version=PROTOCOL_TLSv1)

                # Start
                dt_start = SolBase.datecurrent()

                # Do it
                logger.debug("TcpSimpleClient : __do_ssl_handshake now, self=%s", self)
                self.__do_ssl_handshake()

                # Time
                self._set_ssl_handshake_ms(SolBase.datediff(dt_start))

            # ------------------------
            # POST CONNECT
            # ------------------------

            if self._tcp_client_config.tcp_keepalive_enabled:
                # Switch to TCP KA now
                if self._tcp_client_config.debug_log:
                    logger.info(
                        "TCP KA ON, switching to KA now, (on=%s/delay=%s/failed=%s/interval=%s), self=%s",
                        self._tcp_client_config.tcp_keepalive_enabled,
                        self._tcp_client_config.tcp_keepalive_probes_senddelayms,
                        self._tcp_client_config.tcp_keepalive_probes_failedcount,
                        self._tcp_client_config.tcp_keepalive_probes_sendintervalms,
                        self)
                else:
                    logger.debug(
                        "TCP KA ON, switching to KA now, (on=%s/delay=%s/failed=%s/interval=%s), self=%s",
                        self._tcp_client_config.tcp_keepalive_enabled,
                        self._tcp_client_config.tcp_keepalive_probes_senddelayms,
                        self._tcp_client_config.tcp_keepalive_probes_failedcount,
                        self._tcp_client_config.tcp_keepalive_probes_sendintervalms,
                        self)

                # Go
                self.current_socket.setsockopt(SOL_SOCKET, SO_KEEPALIVE, 1)
                self.current_socket.setsockopt(SOL_TCP, TCP_KEEPIDLE, self._tcp_client_config.tcp_keepalive_probes_senddelayms / 1000)
                self.current_socket.setsockopt(SOL_TCP, TCP_KEEPINTVL, self._tcp_client_config.tcp_keepalive_probes_sendintervalms / 1000)
                self.current_socket.setsockopt(SOL_TCP, TCP_KEEPCNT, self._tcp_client_config.tcp_keepalive_probes_failedcount)

                # Check
                v = self.current_socket.getsockopt(SOL_SOCKET, SO_KEEPALIVE)
                if v != 1:
                    logger.warn("SO_KEEPALIVE mismatch, having=%s, required=1", v)

                v = self.current_socket.getsockopt(SOL_TCP, TCP_KEEPIDLE)
                if v != self._tcp_client_config.tcp_keepalive_probes_senddelayms / 1000:
                    logger.warn("TCP_KEEPIDLE mismatch, having=%s, required=%s", v, self._tcp_client_config.tcp_keepalive_probes_senddelayms / 1000)

                v = self.current_socket.getsockopt(SOL_TCP, TCP_KEEPINTVL)
                if v != self._tcp_client_config.tcp_keepalive_probes_sendintervalms / 1000:
                    logger.warn("TCP_KEEPINTVL mismatch, having=%s, required=%s", v, self._tcp_client_config.tcp_keepalive_probes_sendintervalms / 1000)

                v = self.current_socket.getsockopt(SOL_TCP, TCP_KEEPCNT)
                if v != self._tcp_client_config.tcp_keepalive_probes_failedcount:
                    logger.warn("TCP_KEEPCNT mismatch, having=%s, required=%s", v, self._tcp_client_config.tcp_keepalive_probes_failedcount)

            # Non-blocking mode
            self.current_socket.setblocking(0)
            self.current_socket.settimeout(None)
            logger.debug("TcpSimpleClient : connect : non blocking mode set, self=%s", self)

            # Start the read/write loops
            self._read_greenlet = gevent.spawn(self._read_loop)
            self._write_greenlet = gevent.spawn(self._write_loop)
            logger.debug("TcpSimpleClient : connect : r/w loops started, self=%s", self)

            # Done
            logger.debug("TcpSimpleClient : connect : done, self=%s", self)
            return True

        except Exception as e:
            # Logs
            logger.error("TcpSimpleClient : connect : Exception, ex=%s, self=%s", SolBase.extostr(e), self)

            # Call disconnect
            self._callback_disconnect()

            # Exit
            return False

    # =================================
    # DISCONNECT
    # =================================

    def disconnect(self):
        """
        Disconnect from server. Return true upon success.
        :return True if success, false otherwise.
        """
        try:
            logger.debug("TcpSimpleClient : disconnect : entering, self=%s", self)

            # Check
            if not self.is_connected:
                logger.debug("TcpSimpleClient : disconnect : not connected, doing nothing, self=%s", self)
                return False

            # Disconnect
            if self.current_socket:
                logger.debug("TcpSimpleClient : disconnect : socket.shutdown, self=%s", self)
                # noinspection PyUnusedLocal
                try:
                    self.current_socket.shutdown(2)
                except Exception as e:
                    logger.debug("Exception on shutdown, ex=%s, self=%s", SolBase.extostr(e), self)
                    pass

                logger.debug("TcpSimpleClient : disconnect : socket.close, self=%s", self)
                self.current_socket.close()
                self.current_socket = None

            # Reset socket related context
            self._dt_created = SolBase.datecurrent()
            self._dt_last_recv = self._dt_created
            self._dt_last_send = self._dt_created
            self._disconnect_count.increment()
            self._local_addr = None
            self._local_port = None
            self._remote_addr = None
            self._remote_port = None

            # Signal
            logger.debug("TcpSimpleClient : disconnect : is_connected=False, self=%s", self)
            self.is_connected = False

            # Handle notifications at upper level
            go_call = False

            # Second level callback - Process in lock to avoid multiple notification for same simple client instance
            with self.on_disconnect_notifypoolcallbacklock:
                if self.on_disconnect_notifypoolcallbackcall and self.on_disconnect_notifypoolcallback:
                    logger.debug("TcpSimpleClient : disconnect : calling second level disconnect callback, callback=%s, self=%s", self.on_disconnect_notifypoolcallback, self)
                    self.on_disconnect_notifypoolcallbackcall = False
                    go_call = True

            # Out of lock : call
            # (ASYNC, because we may be called by read/write greenlet, which are going to be killed just after)
            if go_call:
                try:
                    # Call it synchronously (mantis 1907)
                    self.on_disconnect_notifypoolcallback(self)

                    # DO NOT MOVE THIS SLEEP, REDIS WILL DEADLOCK ON START
                    SolBase.sleep(0)
                except Exception as e:
                    logger.warn("on_disconnect_notifypoolcallback : call exception=%s", SolBase.extostr(e))

            # Greenlet reset after is_connected=False (will help to exit itself)
            if self._read_greenlet:
                logger.debug("TcpSimpleClient : disconnect : read kill, self=%s", self)
                self._read_greenlet.kill(GreenletExit, False)
                logger.debug("TcpSimpleClient : disconnect : read kill 1, self=%s", self)
                self._read_greenlet = None
                logger.debug("TcpSimpleClient : disconnect : read kill 2, self=%s", self)

            if self._write_greenlet:
                logger.debug("TcpSimpleClient : disconnect : write kill, self=%s", self)
                self._write_greenlet.kill(GreenletExit, False)
                logger.debug("TcpSimpleClient : disconnect : write kill 1, self=%s", self)
                self._write_greenlet = None
                logger.debug("TcpSimpleClient : disconnect : write kill 2, self=%s", self)

            logger.debug("TcpSimpleClient : disconnect : done, self=%s", self)
            return True

        except Exception as e:
            logger.error("TcpSimpleClient : disconnect : Exception, ex=%s, self=%s", SolBase.extostr(e), self)
            return False

    # =================================
    # SSL HANDSHAKE
    # =================================

    def __do_ssl_handshake(self):
        """
        Do a ssl handshake. Method is using a greenlet, but is blocking for the caller.
        :return: Nothing.
        """

        # Go
        SolBase.sleep(0)
        ssl_greenlet = gevent.spawn(self.__do_ssl_handshake_internal)

        # Wait for complete
        SolBase.sleep(0)
        ssl_greenlet.join()

    def __do_ssl_handshake_internal(self):
        """
        Do a ssl handshake. Call _callback_disconnect() if error.
        :return: Nothing
        """
        try:
            logger.debug("__do_ssl_handshake_internal : start now, self=%s", self)
            SolBase.sleep(0)
            self.current_socket.do_handshake()
        except Exception as e:
            logger.error("__do_ssl_handshake_internal : exception=%s, self=%s", SolBase.extostr(e), self)
            self._callback_disconnect()
        finally:
            SolBase.sleep(0)

    # =================================
    # RECEIVE
    # =================================

    def _on_receive(self, binary_buffer):
        """
        Callback called upon server receive.
        - binary_buffer : a BINARY buffer received on the socket.

        !! CAUTION !!
        - IN ALL CASES THIS METHOD RECEIVE A BINARY BUFFER

        !! CAUTION !!
        - PYTHON 2.X / binary - str    => str is a byte array (or an ascii string) : nothing to do
        - PYTHON 2.X / text - unicode  => you receive a str, you may convert it to unicode (SolBase.binary_to_unicode)

        !! CAUTION !!
        - PYTHON 3.X / binary - bytes  => bytes is binary. You may convert it to a str using encoding.

                |  2.x                     |  3.x
        --------+--------------------------+-----------------------
        Bytes   |  'abc' <type 'str'>      |  b'abc' <type 'bytes'>
        Unicode | u'abc' <type 'unicode'>  |   'abc' <type 'str'>
        :param binary_buffer: Binary buffer received.
        :return: Nothing.
        """

        # Received something...
        logger.debug("TcpSimpleClient : _on_receive : binary_buffer=%s, self=%s", repr(binary_buffer), self)

        # Parse
        self._receive_current_buf = ProtocolParserTextDelimited.parse_protocol(self._receive_current_buf, binary_buffer, self._receive_queue, "\n")

    def get_recv_queue_len(self):
        """
        Get receive queue len
        :return The receive queue length.
        """
        return self._receive_queue.qsize()

    # =================================
    # RECEIVE QUEUE
    # =================================

    def get_from_receive_queue(self, block=False, timeout_sec=None):
        """
        Get a buffer from the receive queue.
        - If block is False, will return an item OR raise an Empty exception if no item.
        - If block is True AND timeOut=None, will wait forever for an item.
        - If block is True and timeout_sec>0, will wait for timeout_sec then raise Empty exception if no item.
        :param block: If true, will block.
        :param timeout_sec: The timeout in second.
        """

        return self._receive_queue.get(block, timeout_sec)
