import io
import os
import errno
import stat
import time
import shutil
import socket
import filecmp
import tempfile
import threading
import six

from kodi_six import xbmc, xbmcgui
from six.moves import urllib_request
from kodi_six.utils import py2_decode

from elementum.taller import follow
from elementum.logger import log
from elementum.osarch import PLATFORM, get_platform
from elementum.config import ELEMENTUMD_HOST
from elementum.addon import ADDON, ADDON_ID, ADDON_PATH, ADDON_VERSION
from elementum.util import (notify, dialog_ok, system_information, toUtf8,
                            download_github_folder, read_current_version, write_current_version,
                            download_current_version,
                            getLocalizedString, getWindowsShortPath, translatePath)

try:
    from ctypes import c_char_p, c_int, cdll
    ctypes_is_supported = True
except Exception as e:
    log.info("Cannot import ctypes: %s" % e)
    ctypes_is_supported = False

try:
    import subprocess
    hasSubprocess = True
    if not PLATFORM["fork"]:
        hasSubprocess = False
except:
    hasSubprocess = False

log_path = ""
custom_path = ""
last_exit_code = -1
binary_platform = {}
repo = "elgatito/elementum-binaries"

lib = None
last_lib = ""

def ensure_exec_perms(file_):
    st = os.stat(file_)
    os.chmod(file_, st.st_mode | stat.S_IEXEC)
    return file_

def android_get_current_appid():
    with open("/proc/%d/cmdline" % os.getpid()) as fp:
        return fp.read().rstrip("\0")

def get_elementumd_checksum(path):
    if not os.path.exists(path):
        return ""

    stats = os.stat(path)
    if stats.st_size < 40:
        return ""

    try:
        with open(path) as fp:
            fp.seek(0, os.SEEK_END)
            fp.seek(fp.tell() - 40, os.SEEK_SET)
            checksum = fp.read().strip()
            return checksum
    except Exception as e:
        log.info("exception reading checksum path %s: %s" % (path, e))
        return ""

def get_addon_profile_dir():
    try:
        return translatePath(ADDON.getAddonInfo("profile")).decode('utf-8')
    except Exception:
        return translatePath(ADDON.getAddonInfo("profile"))

def get_android_data_folders():
    return [
        get_addon_profile_dir(),
        translatePath("special://xbmcbin/"),
        os.path.join("/data", "data", android_get_current_appid()),
        translatePath("special://masterprofile/")
    ]

def get_android_bin_folders():
    return [
        translatePath("special://xbmcbin/"),
        os.path.join("/data", "data", android_get_current_appid()),
        translatePath("special://masterprofile/"),
        get_addon_profile_dir()
    ]

def detect_available_folder(folders):
    for folder in folders:
        if os.path.exists(folder) and is_writable(folder):
            return folder
    return None

def migrate_previous_folder(folders, dest_folder):
    # Check if dest_folder already has application data
    if os.path.exists(os.path.join(dest_folder, "storm.db")) or os.path.exists(os.path.join(dest_folder, "files", ADDON_ID, "storm.db")):
        log.info("Ignoring userdata folder migration due to existing application files in %s folder" % (dest_folder))
        return

    for folder in folders:
        if folder and folder == dest_folder or not os.path.exists(folder):
            continue

        if os.path.exists(os.path.join(folder, "storm.db")):
            source_folder = folder
        elif os.path.exists(os.path.join(folder, "files", ADDON_ID, "storm.db")):
            source_folder = os.path.join(folder, "files", ADDON_ID)
        else:
            continue

        log.info("Migrating previous data folder %s into %s" % (source_folder, dest_folder))
        try:
            shutil.copytree(source_folder, dest_folder)
        except Exception as e:
            log.error("Unable to copy to destination path for update: %s" % e)
            system_information()
            return
        return

def get_elementum_binary():
    global log_path
    global custom_path
    global binary_platform
    binary_platform = get_platform()

    binary = "elementum" + (binary_platform["os"] == "windows" and ".exe" or "")
    binary_dir = os.path.join(ADDON_PATH, "resources", "bin", "%(os)s_%(arch)s" % binary_platform)

    addon_profile_dir = get_addon_profile_dir()

    if binary_platform["os"] == "android":
        log.info("Detected binary folder: %s" % binary_dir)
        binary_dir_legacy = binary_dir.replace("/storage/emulated/0", "/storage/emulated/legacy")
        if "/storage/emulated/legacy" in binary_dir_legacy and os.path.exists(binary_dir_legacy):
            binary_dir = binary_dir_legacy
            log.info("Using changed binary folder for Android: %s" % binary_dir)

        # Folder used only for application data
        xbmc_data_path = detect_available_folder(get_android_data_folders())
        if not xbmc_data_path:
            log.error("Unable to find a folder for application data")
            system_information()
            return False, False
        if ADDON_ID not in xbmc_data_path:
            xbmc_data_path = os.path.join(xbmc_data_path, "files", ADDON_ID)

        # Folder user for binaries
        xbmc_bin_path = detect_available_folder(get_android_bin_folders())
        if not xbmc_bin_path:
            log.error("Unable to find a folder for binary files")
            system_information()
            return False, False
        if ADDON_ID not in xbmc_bin_path:
            xbmc_bin_path = os.path.join(xbmc_bin_path, "files", ADDON_ID)

        log.info("Using folder %s as xbmc_data_path. Using folder %s as xbmc_bin_path" % (xbmc_data_path, xbmc_bin_path))

        # Migrate previous folder to current one to make sure data is not lost
        migrate_previous_folder(get_android_data_folders(), xbmc_data_path)

        dest_binary_dir = os.path.join(xbmc_bin_path, "bin", "%(os)s_%(arch)s" % binary_platform)
        custom_path = xbmc_data_path
        log_path = os.path.join(custom_path, "elementum.log")
    else:
        dest_binary_dir = os.path.join(addon_profile_dir, "bin", "%(os)s_%(arch)s" % binary_platform)

    binary_path = os.path.join(binary_dir, binary)
    dest_binary_path = os.path.join(dest_binary_dir, binary)
# BEGIN ElementumService-patched: version
#    installed_version = read_current_version(binary_dir)
    installed_version = ADDON_VERSION
    binary_dir = ADDON_PATH
    dest_binary_dir = binary_dir
    binary_path = os.path.join(ADDON_PATH, 'fanart.png')
    dest_binary_path = binary_path
# END ElementumService-patched: version

    log.info("Binary detection. Version: %s, Source: %s, Destination: %s" % (installed_version, binary_path, dest_binary_path))

    if not os.path.exists(binary_path) or not installed_version or ADDON_VERSION > installed_version:
        try:
            os.makedirs(binary_dir)
        except OSError as e:
            if e.errno != errno.EEXIST:
                raise

        notify("LOCALIZE[30670];;" + "%(os)s_%(arch)s" % binary_platform)
        if not download_github_folder(repo, "%(os)s_%(arch)s" % binary_platform, binary_dir):
            log.error("Unable to download binary to destination path")
            system_information()
            return False, False
        version = download_current_version(repo)
        write_current_version(binary_dir, version)

    if not os.path.exists(binary_path):
        # notify((getLocalizedString(30103) + " %(os)s_%(arch)s" % PLATFORM), time=7000)
        dialog_ok("LOCALIZE[30347];;" + "%(os)s_%(arch)s" % binary_platform)
        system_information()
        try:
            log.info("Source directory (%s):\n%s" % (binary_dir, os.listdir(os.path.join(binary_dir, ".."))))
            log.info("Destination directory (%s):\n%s" % (dest_binary_dir, os.listdir(os.path.join(dest_binary_dir, ".."))))
        except Exception:
            pass
        return False, False

    if os.path.isdir(dest_binary_path):
        log.warning("Destination path is a directory, expected previous binary file, removing...")
        try:
            shutil.rmtree(dest_binary_path)
        except Exception as e:
            log.error("Unable to remove destination path for update: %s" % e)
            system_information()
            return False, False

    if (
        not os.path.exists(dest_binary_path) or not os.path.exists(binary_path) or get_elementumd_checksum(dest_binary_path) != get_elementumd_checksum(binary_path) or not filecmp.cmp(dest_binary_path, binary_path, shallow=True)
    ):
        log.info(
            "Updating elementum daemon from %s (%s checksum => %s) to %s (%s checksum => %s)"
            % (
                binary_dir,
                binary_path,
                get_elementumd_checksum(binary_path),
                dest_binary_dir,
                dest_binary_path,
                get_elementumd_checksum(dest_binary_path),
            )
        )
        try:
            shutil.rmtree(dest_binary_dir)
        except Exception as e:
            log.error("Unable to remove destination path for update: %s" % e)
            pass
        try:
            os.makedirs(dest_binary_dir)
        except OSError as e:
            log.error("Unable to create destination path for update: %s" % e)
            pass
        if not os.path.exists(dest_binary_dir):
            log.error("Destination path for update does not exist: %s" % dest_binary_dir)
            system_information()
            return False, False
        try:
            for file in os.listdir(binary_dir):
                shutil.copy(
                    os.path.join(binary_dir, file), os.path.join(dest_binary_dir, file)
                )
        except Exception as e:
            log.error("Unable to copy to destination path for update: %s" % e)
            system_information()
            return False, False

    # Clean stale files in the directory, as this can cause headaches on
    # Android when they are unreachable
    dest_files = set(os.listdir(dest_binary_dir))
    orig_files = set(os.listdir(binary_dir))
    log.info("Deleting stale files %s" % (dest_files - orig_files))
    for file_ in (dest_files - orig_files):
        path = os.path.join(dest_binary_dir, file_)
        if os.path.isdir(path):
            shutil.rmtree(path)
        else:
            os.remove(path)

    log.info("Binary detection: [ Source: %s, Destination: %s ]" % (binary_path, dest_binary_path))
# BEGIN ElementumService-patched: binary
#    return dest_binary_dir, ensure_exec_perms(dest_binary_path)
    return 'N/A', 'N/A'
# END ElementumService-patched: binary

def clear_fd_inherit_flags():
    # Ensure the spawned Elementum binary doesn't inherit open files from Kodi
    # which can break things like addon updates. [WINDOWS ONLY]
    try:
        from ctypes import windll

        HANDLE_RANGE = six.moves.xrange(0, 65536)
        HANDLE_FLAG_INHERIT = 1
        FILE_TYPE_DISK = 1

        for hd in HANDLE_RANGE:
            if windll.kernel32.GetFileType(hd) == FILE_TYPE_DISK:
                if not windll.kernel32.SetHandleInformation(hd, HANDLE_FLAG_INHERIT, 0):
                    log.error("Error clearing inherit flag, disk file handle %x" % hd)
    except:
        pass

def jsonrpc_enabled(notify=False):
    try:
        s = socket.socket()
        s.connect(('127.0.0.1', 9090))
        s.close()
        log.info("Kodi's JSON-RPC service is available, starting up...")
        del s
        return True
    except Exception as e:
        log.error(repr(e))
        if notify:
            xbmc.executebuiltin("ActivateWindow(ServiceSettings)")
            dialog = xbmcgui.Dialog()
            dialog.ok("Elementum", getLocalizedString(30199))
    return False

def start_elementumd(monitor, **kwargs):
    global log_path
    global lib
    global last_lib

    jsonrpc_failures = 0
    while jsonrpc_enabled() is False:
        jsonrpc_failures += 1
        log.warning("Unable to connect to Kodi's JSON-RPC service, retrying...")
        if jsonrpc_failures > 1:
            time.sleep(5)
            if not jsonrpc_enabled(notify=True):
                log.error("Unable to reach Kodi's JSON-RPC service, aborting...")
                return None
            else:
                break
        time.sleep(3)

    elementum_dir, elementum_binary = get_elementum_binary()
    elementum_library = "elementum.so"

    log.info("Binary dir: %s, item: %s " % (elementum_dir, elementum_binary))
    if elementum_dir is False or elementum_binary is False:
        return None

# BEGIN ElementumService-patched: stop
#    lockfile = os.path.join(get_addon_profile_dir(), ".lockfile")
    lockfile = os.path.join(get_addon_profile_dir(), '.lockfile')
    if os.path.exists(lockfile):
        os.remove(lockfile)
# END ElementumService-patched: stop
    if os.path.exists(lockfile):
        log.warning("Existing process found from lockfile, killing...")
        try:
            with open(lockfile) as lf:
                pid = int(lf.read().rstrip(" \t\r\n\0"))
            if pid != os.getpid():
                log.info("Killing process id %s" % pid)
                os.kill(pid, 9)
        except OSError as e:
            if e.errno != 3 and e.errno != 22:
                # Ignore:   OSError: [Errno 3] No such process
                log.error(repr(e))
        except Exception as e:
            log.error(repr(e))

        if binary_platform["os"] == "windows":
            try:
                library_lockfile = os.path.join(py2_decode(translatePath(ADDON.getAddonInfo("profile"))), "library.db.lock")
                if os.path.exists(library_lockfile):
                    log.warning("Removing library.db.lock file at %s ..." % library_lockfile)
                    os.remove(library_lockfile)
            except Exception as e:
                log.error(repr(e))

    SW_HIDE = 0
    STARTF_USESHOWWINDOW = 1

    args = [elementum_binary]
    shared_args = ""
    if ADDON.getSetting("local_port") != "":
        args.append("-remotePort=" + ADDON.getSetting("local_port"))
        shared_args += " -remotePort=" + ADDON.getSetting("local_port")
    if ADDON.getSetting("remote_host") != "" and ADDON.getSetting("remote_host") != "127.0.0.1":
        args.append("-localHost=" + ADDON.getSetting("remote_host"))
        shared_args += " -localHost=" + ADDON.getSetting("remote_host")
    if ADDON.getSetting("remote_port") != "":
        args.append("-localPort=" + ADDON.getSetting("remote_port"))
        shared_args += " -localPort=" + ADDON.getSetting("remote_port")
    if ADDON.getSetting("remote_login") != "":
        args.append("-localLogin=" + ADDON.getSetting("remote_login"))
        shared_args += " -localLogin=" + ADDON.getSetting("remote_login")
    if ADDON.getSetting("remote_password") != "":
        args.append("-localPassword=" + ADDON.getSetting("remote_password"))
        shared_args += " -localPassword=" + ADDON.getSetting("remote_password")
    force_library = ADDON.getSetting("local_force_library")
    kwargs["cwd"] = elementum_dir

    if binary_platform["os"] == "windows":
        args[0] = getWindowsShortPath(elementum_binary)
        kwargs["cwd"] = getWindowsShortPath(elementum_dir)
        si = subprocess.STARTUPINFO()
        si.dwFlags = STARTF_USESHOWWINDOW
        si.wShowWindow = SW_HIDE
        clear_fd_inherit_flags()
        kwargs["startupinfo"] = si
        elementum_library = "elementum.dll"
    else:
        env = os.environ.copy()
        env["LD_LIBRARY_PATH"] = "%s:%s" % (elementum_dir, env.get("LD_LIBRARY_PATH", ""))
        kwargs["env"] = env
        kwargs["close_fds"] = True

    wait_counter = 1
    log.debug("Checking for visible")
    while xbmc.getCondVisibility('Window.IsVisible(10140)') or xbmc.getCondVisibility('Window.IsActive(10140)'):
        if wait_counter == 1:
            log.info('Add-on settings currently opened, waiting before starting...')
        if wait_counter > 300:
            break
        time.sleep(1)
        wait_counter += 1

    log.info("elementumd: start args: %s, kw: %s" % (args, kwargs))
    delay = int(ADDON.getSetting("startup_delay"))
    if delay > 0:
        log.info("elementumd: sleeping %d seconds before startup" % (delay))
        time.sleep(delay)

    proc = None
    if force_library != u"true":
        try:
# BEGIN ElementumService-patched: start
#            proc = subprocess.Popen(args, **kwargs)
            from elementum.addon import installAddon, makeAddon, populateGlobals, startApp
            if populateGlobals():
                startApp(args[1:])
            repoAddonInfoFormat = '''<?xml version="1.0" encoding="UTF-8"?><addon provider-name="elgatito" name="{name}" id="{id}" version="0.1.1120001">
  <extension point="xbmc.addon.repository">
    <dir>
      <checksum>{dataDir}addons.xml.md5</checksum>
      <datadir zip="true">{dataDir}</datadir>
      <info compressed="false">{dataDir}addons.xml</info>
    </dir>
  </extension>
  <extension point="xbmc.addon.metadata">
    <platform>all</platform>
    <summary lang="en">{summary}</summary>
  </extension>
</addon>
'''
            serviceRepoAddonDataDir = 'https://StefanIlchev.github.io/ElementumService/'
            if serviceRepoAddonDataDir != '':
                serviceRepoAddonId = 'repository.service.elementum'
                serviceRepoAddonInfo = repoAddonInfoFormat.format(
                    name = 'Elementum Service Repository',
                    id = serviceRepoAddonId,
                    dataDir = serviceRepoAddonDataDir,
                    summary = 'GitHub repository for Elementum Service updates'
                )
                if makeAddon(serviceRepoAddonId, serviceRepoAddonInfo):
                    installAddon(serviceRepoAddonId)
            return None
# END ElementumService-patched: start
        except Exception as e:
            log.error("Unable to start binary: %s" % (e))
            pass
    else:
        log.info("elementumd: Forcing library startup")
    if not proc and ctypes_is_supported:
        try:
            log_dir = translatePath("special://temp/")
            if not log_path:
                log_path = os.path.join(log_dir, "elementum.log")

            # Create and truncate log file before starting the library
            try:
                if not os.path.exists(log_dir):
                    os.makedirs(log_dir)

                log.info("elementumd: truncating log file: %s" % (log_path))
                if not os.path.exists(log_path):
                    open(log_path, 'w').close()
                else:
                    with open(log_path, 'r+') as f:
                        f.truncate()
            except IOError as e:
                log.error("Could not truncate the log: %s" % (e))
                pass

            library_path = os.path.join(elementum_dir, elementum_library)
            # We should not re-load same libraries, so re-using it.
            if last_lib != library_path:
                log.info("elementumd: loading shared library from %s" % library_path)
                lib = cdll.LoadLibrary(library_path)
                last_lib = library_path
            else:
                log.info("elementumd: reusing shared library from %s" % library_path)

            library_thread = threading.Thread(target=start_library, args=[monitor, lib, log_path, shared_args, library_path])
            library_thread.start()

            return library_thread
        except Exception as e:
            log.error("Unable to start library: %s" % e)
            return None

    return proc


def start_library(monitor, lib, log_path, args, library_path):
    global last_exit_code

    if custom_path:
        args = args + " -profilePath=" + custom_path

    log.info("Preparing start with args '%s' and log path: %s" % (args, log_path))

    lib.startWithLogAndArgs.argtypes = [c_char_p, c_char_p]
    lib.startWithLogAndArgs.restype = c_int

    last_exit_code = lib.startWithLogAndArgs(c_char_p(log_path.encode('utf-8')), c_char_p(args.encode('utf-8')))
    log.info("Shared library execution stopped with code %d" % (last_exit_code))

    # Instruct monitor to close and stop waiting for this thread to join
    monitor.close()
    time.sleep(1)

    # It looks that Go does not support dlclose currently - https://github.com/golang/go/issues/11100
    # So commenting out close here.
    # if '.so' in library_path:
    #     try:
    #         lib.dlclose(lib._handle)
    #     except:
    #         pass
    # else:
    #     try:
    #         from ctypes import windll
    #         windll.kernel32.FreeLibrary(lib._handle)
    #     except:
    #         pass

def shutdown():
    try:
        urllib_request.urlopen(ELEMENTUMD_HOST + "/shutdown", timeout=1)
    except Exception:
        pass

def wait_for_abortRequested(proc, monitor):
    monitor.closing.wait()
    log.info("elementumd: exiting elementumd daemon")
    try:
        if not isinstance(proc, threading.Thread):
            proc.terminate()
    except OSError:
        pass  # Process already exited, nothing to terminate
    log.info("elementumd: elementumd daemon exited")

def elementumd_thread(monitor):
    restart_count = 0
    max_restart = 3
    last_code = 0

    try:
        monitor_abort = xbmc.Monitor()  # For Kodi >= 14
        while not monitor_abort.abortRequested():
            # If we ran out of attempts of last exit code was '-9': we do not try to start it again.
            # So if you kill the binary with '-9': it will not be restarted by this monitor.
            if restart_count > max_restart or last_code == -9:
                if monitor.reboot():
                    log.debug("elementumd: resetting attempts")
                    restart_count = 0
                    last_code = 0
                    monitor.reboot(False)
                else:
                    time.sleep(5)

                continue

            log.info("elementumd: starting elementumd")
            proc = start_elementumd(monitor, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
            if proc is None:
                break
            else:
                log.info("elementumd: started process")
                threading.Thread(target=wait_for_abortRequested, args=[proc, monitor]).start()

                if isinstance(proc, threading.Thread):
                    while not os.path.exists(log_path):
                        time.sleep(1)

                    # Forcing start from the very beginning as we truncate the log file on each run
                    for line in follow(io.open(log_path, 'r', encoding='utf8'), start=0, delay=0.5, selector=proc.is_alive):
                        try:
                            log.info(toUtf8(line.rstrip()))
                        except UnicodeDecodeError:
                            log.info(line.rstrip())
                        if not proc.is_alive():
                            break
                    last_code = last_exit_code
                else:
                    if binary_platform["os"] == "windows":
                        while proc.poll() is None:
                            try:
                                log.info(toUtf8(proc.stdout.readline().rstrip()))
                            except UnicodeDecodeError:
                                pass
                            except TypeError:
                                pass
                    else:
                        # Kodi hangs on some Android (sigh...) systems when doing a blocking
                        # read. We count on the fact that Elementum daemon flushes its log
                        # output on \n, creating a pretty clean output
                        import fcntl
                        import select
                        fd = proc.stdout.fileno()
                        fl = fcntl.fcntl(fd, fcntl.F_GETFL)
                        fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
                        while proc.poll() is None:
                            try:
                                to_read, _, _ = select.select([proc.stdout], [], [])
                                for ro in to_read:
                                    line = ro.readline()
                                    if line == "":  # write end is closed
                                        break
                                    try:
                                        log.info(toUtf8(line.rstrip()))
                                    except UnicodeDecodeError:
                                        pass
                                    except TypeError:
                                        pass
                            except IOError:
                                time.sleep(1)  # nothing to read, sleep

                if not isinstance(proc, threading.Thread):
                    last_code = proc.returncode
                    if monitor_abort.abortRequested():
                        break
                    if proc.returncode == 0 or proc.returncode == -9 or proc.returncode == -1:
                        continue

                    if proc.returncode == 5:
                        restart_count = 0
                        notify(getLocalizedString(30332), time=3000)
                    else:
                        restart_count += 1
                        notify(getLocalizedString(30100), time=3000)
                else:
                    log.info("Joining library thread")
                    proc.join()
                    if monitor_abort.abortRequested():
                        break

                    if last_exit_code == 5:
                        restart_count = 0
                        notify(getLocalizedString(30332), time=3000)
                    else:
                        restart_count += 1
                        notify(getLocalizedString(30100), time=3000)

            xbmc.executebuiltin("Dialog.Close(all, true)")
            system_information()

            log.info("Sleeping before next restart attempt")
            time.sleep(5)

            if restart_count >= max_restart:
                log.debug("elementumd: no attempts left")
                notify(getLocalizedString(30110), time=3000)
                continue

    except Exception as e:
        import traceback
        map(log.error, traceback.format_exc().split("\n"))
        notify("%s: %s" % (getLocalizedString(30226), repr(e).encode('utf-8')))
        raise

    log.debug("elementumd: closing")

def is_writable(path):
    try:
        testfile = tempfile.TemporaryFile(dir=path)
        testfile.close()
    except:
        return False
    return True
