#!/usr/bin/env python
import errno
import os
import random
import re
import socket
import subprocess
import sys

import redis
from redis import StrictRedis

import rospy
from std_msgs.msg import Bool

MAX_RETRIES = 100


def is_socket_free(host, port):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    result = sock.connect_ex((host, port))
    return result != 0


class RedisServer:
    def __init__(self):
        rospy.init_node("redis_server", anonymous=True)

        self._gone_down = False
        self._ready = False
        self._redis_process = None

        self._ready_pub = rospy.Publisher(
            '~ready', Bool, latch=True, queue_size=1
        )

        self.test_mode = rospy.get_param("~test_mode", False)

        default_host = 'localhost'
        if self.test_mode:
            default_port = self._find_free_port(default_host, 49152, 65535)
        else:
            default_port = 6379
        is_master = rospy.get_param('~master', True)

        if is_master:
            self._redis_host = rospy.get_param('redis_host', default_host)
            rospy.set_param('redis_host', self._redis_host)
            self._redis_port = rospy.get_param('redis_port', default_port)
            rospy.set_param('redis_port', self._redis_port)
            default_config = ''
            self._redis_config = rospy.get_param('redis_config', default_config)
            rospy.set_param('redis_config', self._redis_config)
        else:
            self._redis_host = rospy.get_param('~host')
            self._redis_port = rospy.get_param('~port')
            self._redis_config = rospy.get_param('~config')

        rospy.loginfo(
            f"Redis server address: {self._redis_host}:{self._redis_port}"
        )
        rospy.loginfo(f"Redis config: {self._redis_config}")

        self._check_if_redis_server_is_installed()

        rospy.on_shutdown(self._on_node_shutdown)

        self._server_loop()

    @staticmethod
    def _find_free_port(host, min_port, max_port):
        default_port = random.randrange(min_port, max_port)

        count = 0
        while not is_socket_free(host, default_port):
            default_port = random.randrange(min_port, max_port)
            count += 1
            if count > MAX_RETRIES:
                rospy.logerr(
                    "Can't find a free port to run the test server on."
                )
                sys.exit(1)
        return default_port

    def _check_if_redis_server_is_installed(self):
        try:
            redisv = subprocess.check_output(
                ['redis-server', '--version'], universal_newlines=True
            )
            match = re.search(r'.*v=(\d+\.\d+\.\d+) .*', redisv)
            self._redis_version = match.group(1)
        except subprocess.CalledProcessError:
            rospy.logerr("Can't find Redis server executable.")
            sys.exit(1)
        rospy.loginfo(f"Found Redis server version {self._redis_version}")

    def _on_node_shutdown(self):
        rospy.loginfo("Shutting down Redis server")
        if self._gone_down:
            rospy.logwarn("It looks like Redis server already died.")
        elif self._redis_process:
            self._redis_process.terminate()

    def _check_ready(self):
        redis_con = StrictRedis(host=self._redis_host, port=self._redis_port)
        try:
            redis_con.ping()
        except redis.ConnectionError:
            return False
        else:
            return True

    def _server_loop(self):
        def block_kill():
            os.setpgrp()

        cmd = ['redis-server']
        if self._redis_config:
            cmd.append(str(self._redis_config))
        cmd += ['--port', str(self._redis_port)]

        self._redis_process = subprocess.Popen(
            cmd,
            stdout=subprocess.PIPE,
            preexec_fn=block_kill,
            universal_newlines=True,
        )
        while self._redis_process.poll() is None:
            try:
                stdout = self._redis_process.stdout.readline()
            except OSError as e:
                if e.errno == errno.EINTR:
                    continue
                else:
                    raise e

            if stdout is not None:
                if 'ERROR' in stdout:
                    rospy.logerr(stdout.strip())
                else:
                    rospy.loginfo(stdout.strip())

            if not self._ready and self._check_ready():
                self._ready = True
                self._ready_pub.publish(Bool(True))

        if not rospy.is_shutdown():
            rospy.logerr("Redis server process stopped")
            self._ready_pub.publish(Bool(False))

        if self._redis_process.returncode != 0:
            rospy.logerr(
                f"Redis server process error. Exit code {self._redis_process.returncode}"
            )

        self._gone_down = True
        self._ready = False


if __name__ == '__main__':
    server = RedisServer()
