# -*- coding: utf-8 -*-

# Copyright 2008-2016 Mir Calculate. http://www.calculate-linux.org
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.


from .files import (process, checkUtils, readFile, listDirectory,
                   getRunCommands, getProgPath, FilesError)
from . import device
import sys
import os
import re
import struct
import fcntl
import socket
import math
import ctypes
from os import path
import select
import time

from ..cl_lang import setLocalTranslate
_ = lambda x: x
setLocalTranslate('cl_lib3', sys.modules[__name__])

PROCFS_NET_PATH = "/proc/net/dev"

# From linux/sockios.h
SIOCGIFINDEX = 0x8933
SIOCGIFFLAGS = 0x8913
SIOCSIFFLAGS = 0x8914
SIOCSIFHWADDR = 0x8924
SIOCSIFADDR = 0x8916
SIOCSIFNETMASK = 0x891C
SIOCETHTOOL = 0x8946

SIOCGIFADDR = 0x8915
SIOCGIFNETMASK = 0x891B
SIOCGIFHWADDR = 0x8927

# Resources allocated
IFF_RUNNING = 0x40
IFF_MASTER = 0x400
IFF_SLAVE = 0x800

# ip digit from 0|1-255|254 (template)
IP_DIG = "[%s-9]|(?:1[0-9]|[1-9])[0-9]|2[0-4][0-9]|25[0-%s]"
# ip net 0-32
IP_NET_SUFFIX = "[0-9]|[12][0-9]|3[012]"
# ip digs 1-254,0-254,0-255
IP_DIGS = {'dig1_254': IP_DIG % (1, 4), 'dig0_254': IP_DIG % (0, 4),
           'dig0_255': IP_DIG % (0, 5), }
# ip addr 10.0.0.12
IP_ADDR = "(%(dig1_254)s)\.(%(dig0_254)s)\.(%(dig0_254)s)\.(%(dig1_254)s)" % \
          IP_DIGS
IP_MASK = "(%(dig0_255)s)\.(%(dig0_255)s)\.(%(dig0_255)s)\.(%(dig0_255)s)" % \
          IP_DIGS
# ip addr for net 10.0.0.0
IP_NET = "(%(dig1_254)s)\.(%(dig0_254)s)\.(%(dig0_254)s)\.(%(dig0_254)s)" % \
         IP_DIGS
# ip and net 192.168.0.0/16
IP_ADDR_NET = "(%(ipaddr)s)/((%(ipnet)s))" % {'ipaddr': IP_NET,
                                              'ipnet': IP_NET_SUFFIX}

reIp = re.compile("^{0}$".format(IP_ADDR))
reNetSuffix = re.compile("^{0}$".format(IP_NET_SUFFIX))
reNet = re.compile("^{0}$".format(IP_ADDR_NET))
reMask = re.compile("^{0}$".format(IP_MASK))


def checkIp(ip):
    """Check ip"""
    return reIp.match(ip)


def checkNetSuffix(netSuffix):
    """Check net suffix"""
    return reNetSuffix.match(netSuffix)


def checkNet(net):
    """Check net"""
    if not reNet.match(net):
        return False
    ip, op, cidr = net.partition('/')
    mask = strIpToIntIp(cidrToMask(int(cidr)))
    return (strIpToIntIp(ip) & mask) == (strIpToIntIp(ip))


maskDigs = [str(x) for x 
    in (0b10000000, 0b11000000, 0b11100000, 0b11110000,
        0b11111000, 0b11111100, 0b11111110, 0b11111111)]

def checkMask(mask):
    """Check net"""
    if not mask:
        return False
    if mask.count('.') != 3:
        return False
    zero = False
    for dig in mask.split('.'):
        if zero or not dig in maskDigs:
            if dig == "0":
                zero = True
            else:
                return False
    return True


def getIpAndMask(interface="eth0"):
    """Get ip and mask from interface"""
    ifconfig = process('/sbin/ifconfig', interface)
    res = re.search(r"inet addr:(\S+)\s.*Mask:(\S+)", ifconfig.read(), re.S)
    if res:
        return res.groups()
    else:
        return "", ""


def strIpToIntIp(addr):
    """Convert ip specified by string to integer"""
    addr = addr.split('.')
    return ((int(addr[0]) << 24) |
            (int(addr[1]) << 16) |
            (int(addr[2]) << 8) |
            (int(addr[3])))

def intIpToStrIp(addr):
    """Convert ip specified by integer to string"""
    return "{0}.{1}.{2}.{3}".format(
        addr >> 24, (addr >> 16) & 0xff, (addr >> 8) & 0xff, addr & 0xff)


def numMaskToCidr(netmask):
    """
    Convert integer mask to cidr
    """
    neg_net = ctypes.c_uint32(~netmask).value
    return 32 - int(math.log(neg_net, 2)) - 1 if neg_net else 32


def maskToCidr(mask):
    """Convert mask specified by str to net"""
    mask = strIpToIntIp(mask)
    return numMaskToCidr(mask)


def cidrToMask(cidr):
    """Convert net to mask specified by str"""
    return intIpToStrIp((2 ** cidr - 1) << (32 - cidr))


def getIpNet(ip, mask=None, cidr=None):
    """Get net (xx.xx.xx.xx/xx) by ip address and mask"""
    ip = strIpToIntIp(ip)
    if not mask is None:
        net = maskToCidr(mask)
    else:
        net = int(cidr)
        mask = cidrToMask(net)
    mask = strIpToIntIp(mask)
    return "{ip}/{net}".format(ip=intIpToStrIp(ip & mask),
                               net=net)


def isIpInNet(checkip, *ipnets):
    """Check is ip in specified nets"""
    return [x[0] for x 
        in [(y[0], y[1][0], strIpToIntIp(cidrToMask(int(y[1][1])))) for y 
            in [(z, z.partition('/')[0::2]) for z 
                in ipnets]] 
        if strIpToIntIp(checkip) & x[2] == strIpToIntIp(x[1]) & x[2]]

def isUsingNetworkManager():
    try:
        p = process("/usr/bin/nmcli", "general", "status")
        return p.success()
    except FilesError:
        return False

def isNMDhcp(interface="eth0"):
    p = process("/usr/bin/nmcli", "-g", "ipv4.method", "connection", "show", "eth0")
    if p.success():
        if "auto" in p.read():
            return True
    return False

def isDhcpIp(interface="eth0"):
    """Get ip by dhcp or static"""
    # dhclients (dhcpcd, dhclient (dhcp), udhcpc (busybox)
    if isUsingNetworkManager() and isNMDhcp(interface):
        return True
    commands = getRunCommands()
    dhcpProgs = ("dhcpcd", "dhclient", "udhcpc")
    if [x for x in commands if interface in x and any(prog in x for prog in dhcpProgs)]:
        return True
    else:
        # если запущен демон dhcpcd
        if [x for x in commands if "dhcpcd\x00-q" in x]:
            curIp = getIp(interface)
            dhcpcd = getProgPath('/sbin/dhcpcd')
            leaseIp = [x.group(1) for x 
                in map(re.compile('^ip_address=(.*)$').search, 
                    process(dhcpcd, '-U', interface)) if x]
            if not curIp or leaseIp and leaseIp[0] == curIp:
                return True
        return False


def getRouteTable(onlyIface=()):
    """Get route table, exclude specifed iface"""
    ipProg = checkUtils('/sbin/ip')
    routes = process(ipProg, "route")
    if onlyIface:
        filterRe = re.compile("|".join(("dev %s" % x for x in onlyIface)))
        routes = filter(filterRe.search, routes)
    for line in routes:
        network, op, line = line.partition(" ")
        routeParams = [x.strip() for x in line.split()]
        # (network,{'via':value,'dev':value})
        if network:
            yield (network, dict(zip(routeParams[0::2], routeParams[1::2])))


def getInterfaces():
    """
    Get available interfaces (discard which hasn't device)
    """

    def filter_ethernet(x):
        # exclude loopback
        if device.sysfs.read(x, "type").strip() != "1":
            return False
        # physical device
        if device.sysfs.exists(x, "device"):
            return True
        # exclude bridges
        if device.sysfs.exists(x, "brport"):
            return False
        if device.sysfs.exists(x, "bridge"):
            return False
        # exclude bonding
        if device.sysfs.exists(x, "bonding"):
            return False
        if device.sysfs.exists(x, "bonding_slave"):
            return False
        # exclude slave devices
        if any(device.sysfs.glob(x, "lower_*")):
            return False
        return True

    return sorted(path.basename(x)
                  for x in device.sysfs.listdir(
                    device.sysfs.Path.ClassNet, fullpath=True)
                  if filter_ethernet(x))

def getMaster(iface):
    master_path = [device.sysfs.Path.ClassNet, iface, "master"]
    if device.sysfs.exists(*master_path):
        return path.split(device.sysfs.realpath(*master_path))[-1]
    return None

def getIp(iface):
    sockfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    ifreq = struct.pack('16sH14s', iface.encode("UTF-8"), socket.AF_INET, b'\x00' * 14)
    try:
        res = fcntl.ioctl(sockfd, SIOCGIFADDR, ifreq)
    except IOError:
        return ""
    finally:
        sockfd.close()
    ip = struct.unpack('16sH2x4s8x', res)[2]
    return socket.inet_ntoa(ip)

def checkFlag(iface, flag):
    sockfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    ifreq = struct.pack('16sH14s', iface.encode("UTF-8"), socket.AF_INET, b'\x00' * 14)
    try:
        res = fcntl.ioctl(sockfd, SIOCGIFFLAGS, ifreq)
    except IOError:
        return False
    finally:
        sockfd.close()
    return bool(struct.unpack('16sH2x4s8x', res)[1] & flag)

def getPlugged(iface):
    return checkFlag(iface, IFF_RUNNING)

def isSlaveInterface(iface):
    return checkFlag(iface, IFF_SLAVE)

def isMasterInterface(iface):
    return checkFlag(iface, IFF_MASTER)

def getMask(iface):
    """
    Get mask for interface
    """
    sockfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    ifreq = struct.pack('16sH14s', iface.encode("UTF-8"), socket.AF_INET, b'\x00' * 14)
    try:
        res = fcntl.ioctl(sockfd, SIOCGIFNETMASK, ifreq)
    except IOError:
        return 0
    finally:
        sockfd.close()
    netmask = socket.ntohl(struct.unpack('16sH2xI8x', res)[2])
    return numMaskToCidr(netmask)

def isWireless(iface):
    """
    Является ли данное устройство беспроводным
    """
    return device.sysfs.exists(device.sysfs.Path.ClassNet, iface, "wireless")

def getMac(iface):
    """
    Get mac for interface
    """
    sockfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    ifreq = struct.pack('16sH14s', iface.encode("UTF-8"), socket.AF_UNIX, b'\x00' * 14)
    try:
        res = fcntl.ioctl(sockfd, SIOCGIFHWADDR, ifreq)
    except IOError:
        return ""
    address = struct.unpack('16sH14s', res)[2]
    mac = struct.unpack('6B8x', address)
    sockfd.close()
    return ":".join(['%02X' % i for i in mac])


def getOperState(iface):
    """
    Get interface state up or down
    """
    if device.sysfs.read(device.sysfs.Path.ClassNet, iface, "operstate") == "down":
        return "down"
    return "up"


def isOpenPort(ip, port):
    """
    Test if an [ip:port] is open
    """
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    try:
        s.connect((ip, int(port)))
        s.shutdown(2)
        return True
    except Exception:
        return False


class IPError(Exception):
    """Error received on work with ip"""
    pass


class Pinger():
    # ICMP parameters
    ICMP_ECHO = 8  # Echo request (per RFC792)
    ICMP_MAX_RECV = 2048  # Max size of incoming buffer

    def checksum(self, source_string):
        """
        A port of the functionality of in_cksum() from ping.c
        Ideally this would act on the string as a series of 16-bit ints (host
        packed), but this works.
        Network data is big-endian, hosts are typically little-endian
        """
        countTo = (int(len(source_string) / 2)) * 2
        sum = 0
        count = 0

        # Handle bytes in pairs (decoding as short ints)
        while count < countTo:
            #note: no need for ord(bytes[i]) in py3, bytes already 
            # return int values when accessed via indeces
            if sys.byteorder == "little":
                loByte = source_string[count]
                hiByte = source_string[count + 1]
            else:
                loByte = source_string[count + 1]
                hiByte = source_string[count]
            sum += hiByte * 256 + loByte
            count += 2

        # Handle last byte if applicable (odd-number of bytes)
        # Endianness should be irrelevant in this case
        if countTo < len(source_string):  # Check for odd length
            loByte = ord(source_string[len(source_string) - 1])
            sum += loByte

        # Truncate sum to 32 bits (a variance from ping.c,which
        sum &= 0xffffffff
        # uses signed ints, but overflow is unlikely in ping)

        sum = (sum >> 16) + (sum & 0xffff)  # Add high 16 bits to low 16 bits
        sum += (sum >> 16)  # Add carry from above (if any)
        answer = ~sum & 0xffff  # Invert and truncate to 16 bits
        answer = socket.htons(answer)

        return answer

    def ping(self, destIP, timeout, numDataBytes):
        """
        Returns either the delay (in ms) or None on timeout.
        """
        delay = None

        try:  # One could use UDP here, but it's obscure
            mySocket = socket.socket(socket.AF_INET, socket.SOCK_RAW,
                                     socket.getprotobyname("icmp"))
        except socket.error as e:
            raise IPError(_("failed. (socket error: '%s')" % e.args[1]))

        my_ID = os.getpid() & 0xFFFF

        sentTime = self.send_one_ping(mySocket, destIP, my_ID, 0,
                                      numDataBytes)
        if sentTime is None:
            mySocket.close()
            return delay

        recvTime, dataSize, iphSrcIP, icmpSeqNumber, iphTTL = \
            self.receive_one_ping(mySocket, my_ID, timeout)

        mySocket.close()

        if recvTime:
            delay = (recvTime - sentTime) * 1000
            return (dataSize, socket.inet_ntoa(struct.pack("!I", iphSrcIP)),
                    iphTTL, delay)
        else:
            raise IPError(_("Request timed out"))

    def send_one_ping(self, mySocket, destIP, myID, mySeqNumber, numDataBytes):
        """
        Send one ping to the given >destIP<.
        """
        try:
            destIP = socket.gethostbyname(destIP)
        except socket.gaierror as e:
            raise IPError(e.strerror)

        # Header is type (8), code (8), checksum (16), id (16), sequence (16)
        myChecksum = 0

        # Make a dummy heder with a 0 checksum.
        header = struct.pack(
            "!BBHHH", self.ICMP_ECHO, 0, myChecksum, myID, mySeqNumber
        )

        padBytes = []
        startVal = 0x42
        for i in range(startVal, startVal + numDataBytes):
            padBytes += [(i & 0xff)]  # Keep chars in the 0-255 range
        data = bytes(padBytes)

        # Calculate the checksum on the data and the dummy header.
        # Checksum is in network order
        myChecksum = self.checksum(header + data)

        # Now that we have the right checksum, we put that in. It's just easier
        # to make up a new header than to stuff it into the dummy.
        header = struct.pack(
            "!BBHHH", self.ICMP_ECHO, 0, myChecksum, myID, mySeqNumber
        )

        packet = header + data

        sendTime = time.time()

        try:
            # Port number is irrelevant for ICMP
            mySocket.sendto(packet, (destIP, 1))
        except socket.error as e:
            raise IPError("General failure (%s)" % (e.args[1]))

        return sendTime

    def receive_one_ping(self, mySocket, myID, timeout):
        """
        Receive the ping from the socket. Timeout = in ms
        """
        timeLeft = timeout / 1000.0

        while True:  # Loop while waiting for packet or timeout
            startedSelect = time.time()
            whatReady = select.select([mySocket], [], [], timeLeft)
            howLongInSelect = (time.time() - startedSelect)
            if isinstance(whatReady[0], list) and not whatReady[0]:  # Timeout
                return None, 0, 0, 0, 0

            timeReceived = time.time()

            recPacket, addr = mySocket.recvfrom(self.ICMP_MAX_RECV)

            ipHeader = recPacket[:20]
            iphVersion, iphTypeOfSvc, iphLength, \
            iphID, iphFlags, iphTTL, iphProtocol, \
            iphChecksum, iphSrcIP, iphDestIP = struct.unpack(
                "!BBHHHBBHII", ipHeader
            )

            icmpHeader = recPacket[20:28]
            icmpType, icmpCode, icmpChecksum, \
            icmpPacketID, icmpSeqNumber = struct.unpack(
                "!BBHHH", icmpHeader
            )

            if icmpPacketID == myID:  # Our packet
                dataSize = len(recPacket) - 28
                return timeReceived, dataSize, iphSrcIP, icmpSeqNumber, iphTTL

            timeLeft = timeLeft - howLongInSelect
            if timeLeft <= 0:
                return None, 0, 0, 0, 0

def check_port(target, port, timeout=5):
    """
    Проверить порт (port) на хосте (target)
    :param target: хост
    :param port: порт
    :param timeout: таймаут
    :return: True - порт открыт, False - нет хоста, порт закрыт, firewall
    """
    try:
        targetIP = socket.gethostbyname(target)
    except socket.gaierror:
        return False

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(timeout)
    try:
        s.connect((targetIP, port))
        s.close()
    except (socket.error, socket.timeout):
        return False
    return True
