#
# public domain
#
__doc__ = """
simple sam i2p socket implemenation
"""

from i2p import datatypes
from enum import Enum

import collections
import logging
import os
import string
import random
import socket as pysocket

from contextlib import wraps

Error = pysocket.error

class State(Enum):
    """
    state of a sam socket
    """
    
    Initial = 0
    """
    we are not connected to anything, initial state
    """
    
    Established = 1
    """
    we have established a session with sam
    """
    
    Ready = 2
    """
    we are ready to do stuff
    """
    
    Connecting = 3
    """
    we are connecting or binding
    """
    
    Running = 4
    """
    we are connected or bound
    """
    
    Closing = 5
    """
    closing all connections
    """
    
    Closed = 6
    """
    sam connection closed entirely
    """    
    
    Error = 7
    """
    an error occured
    """
    
class SAM:
    """
    decorator constants
    """
    SOCK_STREAM = datatypes.I2CPProtocol.STREAMING
    SOCK_DGRAM = datatypes.I2CPProtocol.DGRAM
    SOCK_RAW = datatypes.I2CPProtocol.RAW

def randnick(l):
    nick = ''
    for n in range(l):
        nick += random.choice(string.ascii_letters)
    return nick
    
def _sam_readline(sock):
    """
    read a line from a sam control socket
    """
    response = bytearray()
    while True:
        c = sock.recv(1)
        if c:
            if c == b'\n':
                break
            response += c
        else:
            break
    return response.decode('ascii')

SamReply = collections.namedtuple('SamReply', ['cmd', 'opts'])

def _sam_parse_reply(line):
    """
    parse a reply line into a dict
    """
    opts = dict()
    parts = line.split(' ')

    for part in parts[2:]:
        if '=' in part:
            idx = part.index('=')
            k = part[:idx]
            v = part[1+idx:]
            opts[k] = v
    return SamReply(parts[0], opts)

def _sam_cmd(sock, line):
    """
    do a sam command on a socket
    :returns result:
    """
    #print ('sam --> '+line)
    sock.send(bytearray(line, 'ascii'))
    sock.send(b'\n')
    _line = _sam_readline(sock)
    #print ('sam <-- '+_line)
    return _sam_parse_reply(_line)
    
class Socket(object):
    """
    base of all sam socket objects
    """

    _log = logging.getLogger("i2p.socket.sam.simple.BaseSocket")
    _dest_cache = dict()
    
    def samState(*states):
        def f(func):
            def check_state(self, *args_inner, **kwargs_inner):
                if self._state in states:
                    return func(self, *args_inner, **kwargs_inner)
                else:
                    raise Exception("invalid socket state: {}".format(self._state))
            return check_state
        return f

    def samType(*types):
        def f(func):
            def check_type(self, *args, **kwargs):
                if self._type in types:
                    return func(self, *args, **kwargs)
                else:
                    raise Exception("invalid socket type: {}".format(self._type))
            return check_type
        return f

    def samConnect(func):
        def check_connect(self, *args, **kwargs):
            if self._samSocket is None:
                self._log.info("connecting to sam")
                # we are not connected
                samSocket = pysocket.socket()
                samSocket.setsockopt(pysocket.IPPROTO_TCP, pysocket.SO_KEEPALIVE, 1)
                try:
                    samSocket.connect(self._samAddr)
                    self._samHandshake(samSocket)
                except pysocket.timeout as ex:
                    raise ex
                except pysocket.error as ex:
                    raise ex
                else:
                    self._samSocket = samSocket
            return func(self, *args, **kwargs)
        return check_connect
    
    def __init__(self, samaddr, dgramAddr, dgramBind, socketType):
        """
        :param samaddr: the socket address for sam
        """
        if samaddr:
            self._samAddr = samaddr
            self._samDgramAddr = dgramAddr
            self._dgram_bind = dgramBind
            self._samSocket = None
            self._data_sock = None
            self._state = State.Initial
            self._type = socketType
            self.dest = None
        else:
            raise ValueError("samaddr must not be None")

    
        
        
                
    def _samHandshake(self, sock):
        """
        handshake with sam via a socket.socket instance
        """
        repl = _sam_cmd(sock, 'HELLO VERSION MIN=3.0 MAX=3.2')
        if repl.opts['RESULT'] == 'OK':
            self._state = State.Established
        else:
            raise Exception("failed to handshake with SAM: {}".format(repl.opts["RESULT"]))
        
        
    @samConnect
    @samState(State.Established)
    @samType(SAM.SOCK_STREAM)
    def connect(self, addr, **i2cpOptions):
        """
        connect to a remote endpoint
        :param addr: the remote endpoint to connect to, either a destination/name or a (destination/name, port) tuple
        :param keyfile: the file containing the private keys to use or None for transient
        :param nickname: the nickname to use for tunnels or None for random decided by sam
        :param i2cpOptions: additional i2cp options to pass into sam
        """
        self._log.info('connect')
        nickname = randnick(7)
        self.bind(None, nickname, **i2cpOptions)
        if isinstance(addr, tuple):
            addr = addr[0]
        dest = self.lookup(addr)
        # new socket
        self._data_sock = pysocket.socket()
        self._data_sock.connect(self._samAddr)
        # say hello
        repl = _sam_cmd(self._data_sock, 'HELLO VERSION MIN=3.0 MAX=3.2')
        # send connect
        cmd = 'STREAM CONNECT ID={} DESTINATION={} SILENT=false'.format(nickname, dest)
        if repl.opts["RESULT"] == "OK":
            repl = _sam_cmd(self._data_sock, cmd)
            self._state = State.Running
        else:
            self._state = State.Error

    def getsocketinfo(self):
        return self.dest
            
    @samConnect
    @samState(State.Established)
    @samType(SAM.SOCK_STREAM, SAM.SOCK_DGRAM)
    def bind(self, keyfile, nickname=None, **i2cpOptions):
        """
        bind to an address
        :param keyfile: the file containing the private keys to use
        :param nickname: the nickname to use for tunnels or None for random decided by sam
        :param i2cpOptions: additional i2cp options to pass into sam