import json
import os
import time
import xml.etree.cElementTree as ElementTree
from gzip import GzipFile
from io import StringIO
from urllib.parse import quote_plus
from urllib.request import Request

import requests

from fsgs.network import is_http_url, openretro_url_prefix
from fsgs.res import gettext


def bytes_to_int(m):
    return m[0] << 24 | m[1] << 16 | m[2] << 8 | m[3]


class GameDatabaseSynchronizer(object):
    username = ""
    password = ""

    def __init__(
        self,
        context,
        client,
        on_status=None,
        stop_check=None,
        platform_id="amiga",
    ):
        self.context = context
        if client:
            self.client = client
            self.database = client.database
        # self.downloaded_size = 0
        self.on_status = on_status
        self._stop_check = stop_check
        if "/" in platform_id:
            self.host, self.platform_id = platform_id.split("/")
        else:
            self.host = ""
            self.platform_id = platform_id.lower()

    def stop_check(self):
        if self._stop_check:
            return self._stop_check()

    def set_status(self, title, status=""):
        if self.on_status:
            self.on_status((title, status))

    def auth(self):
        return (self.username, self.password)

    def synchronize(self):
        if "database" not in self.context.meta:
            # we haven't looked up synchronization information from the server,
            # that probably means we didn't want to synchronize with the
            # server now, therefore we just return
            return

        self._synchronize()
        if self.stop_check():
            self.client.database.rollback()
        else:
            print("committing data")
            self.set_status(
                gettext("Updating database"), gettext("Committing data...")
            )
            self.database.commit()
            print("done")

        if os.environ.get("FSGS_WRITE_DAT_FILES", "") == "1":
            self.write_dat_file()

    def write_dat_file(self):
        games = {}
        descriptions = {}

        for uuid in self.database.get_all_uuids():
            values = self.database.get_game_values_for_uuid(
                uuid, recursive=False
            )
            file_list_str = values.get("file_list", "")
            if not file_list_str:
                continue
            file_list = json.loads(file_list_str)
            for entry in file_list:
                # print(file_list)
                if entry["name"].endswith("/"):
                    continue
                # game = entry["sha1"][:3].upper()
                # game = uuid[:8] + "/" + values["game_name"]
                # games.setdefault(game, {})[entry["sha1"]] = {
                #     "name": entry["sha1"],
                #     "sha1": entry["sha1"],
                #     "size": entry["size"],
                # }
                game = uuid
                description = values.get("x_name", "")
                descriptions[game] = description
                games.setdefault(game, {})[entry["name"]] = {
                    "name": entry["name"],
                    "sha1": entry["sha1"],
                    "size": entry["size"],
                }

        xml_path = os.path.splitext(self.database.path())[0] + ".xml"
        datafile_node = ElementTree.Element("datafile")
        datafile_node.text = "\n\t"

        header_node = ElementTree.Element("header")
        datafile_node.append(header_node)
        header_node.text = "\n\t\t"
        header_node.tail = "\n\t"

        name_node = ElementTree.Element("name")
        header_node.append(name_node)
        name_node.text = self.database.name()
        name_node.tail = "\n\t\t"

        description_node = ElementTree.Element("description")
        header_node.append(description_node)
        description_node.text = self.database.name()
        description_node.tail = "\n\t\t"

        version_node = ElementTree.Element("version")
        header_node.append(version_node)
        version_node.text = "2000-01-01"  # FIXME
        version_node.tail = "\n\t\t"

        author_node = ElementTree.Element("author")
        header_node.append(author_node)
        author_node.text = "Auto-generated by OpenRetro Launcher"
        author_node.tail = "\n\t"

        for game_name in sorted(games.keys()):
            game = games[game_name]
            game_node = ElementTree.Element("game")
            datafile_node.append(game_node)
            game_node.attrib["name"] = game_name
            game_node.text = "\n\t\t"
            game_node.tail = "\n\t"

            description_node = ElementTree.Element("description")
            game_node.append(description_node)
            description_node.text = descriptions[game_name]
            description_node.tail = "\n\t\t"

            for i, rom_name in enumerate(sorted(game.keys())):
                rom = game[rom_name]
                rom_node = ElementTree.Element("rom")
                if i == len(game) - 1:
                    rom_node.tail = "\n\t"
                else:
                    rom_node.tail = "\n\t\t"
                game_node.append(rom_node)
                rom_node.attrib["name"] = rom["name"]
                rom_node.attrib["size"] = str(rom["size"])
                rom_node.attrib["sha1"] = rom["sha1"]

        # def indent(elem, level=0):
        #     i = "\n" + level*"  "
        #     j = "\n" + (level-1)*"  "
        #     if len(elem):
        #         if not elem.text or not elem.text.strip():
        #             elem.text = i + "  "
        #         if not elem.tail or not elem.tail.strip():
        #             elem.tail = i
        #         for subelem in elem:
        #             indent(subelem, level+1)
        #         if not elem.tail or not elem.tail.strip():
        #             elem.tail = j
        #     else:
        #         if level and (not elem.tail or not elem.tail.strip()):
        #             elem.tail = j
        #     return elem

        # indent(datafile_node)
        tree = ElementTree.ElementTree(datafile_node)
        # tree.
        # tree.write('output.xml')
        # datafile_node('output.xml')
        # tree.write(xml_path)
        with open(xml_path, "wb") as f:
            f.write(b'<?xml version="1.0" encoding="UTF-8"?>\n')
            f.write(
                b'<!DOCTYPE datafile PUBLIC "-//Logiqx//DTD ROM Management Datafile//EN" "http://www.logiqx.com/Dats/datafile.dtd">\n'
            )
            f.write(b"\n")
            tree.write(f)

    def _synchronize(self):
        if (
            self.context.meta["database"]["version"]
            != self.database.get_game_database_version()
        ):
            self.set_status(gettext("Resetting game database..."))
            self.database.clear()
            self.database.set_game_database_version(
                self.context.meta["database"]["version"]
            )

        self.set_status(gettext("Synchronizing game database..."))

        while True:
            if self.stop_check():
                return
            data = self.fetch_game_sync_data()
            if self.stop_check():
                return
            if not data:
                print("no more changes")
                break

            t1 = time.time()
            k = 0
            while k < len(data):
                game_sync_id = bytes_to_int(data[k : k + 4])
                k += 4
                game_uuid = data[k : k + 16]
                k += 16
                game_data_size = bytes_to_int(data[k : k + 4])
                k += 4
                game_data = data[k : k + game_data_size]
                k += game_data_size
                # print("game data len:", len(game_data))
                if len(game_data) > 0:
                    self.database.add_game(game_sync_id, game_uuid, game_data)
                else:
                    self.database.delete_game(game_sync_id, game_uuid)
            t2 = time.time()
            print("  {0:0.2f} seconds".format(t2 - t1))

        last_json_data = ""
        while True:
            if self.stop_check():
                return
            json_data = self.fetch_rating_entries()
            if self.stop_check():
                return
            if json_data == last_json_data:
                print("no more changes")
                break
            last_json_data = json_data
            num_changes = len(json_data["ratings"])
            print("  processing {0} entries".format(num_changes))
            t1 = time.time()
            for update in json_data["ratings"]:
                cursor = self.client.database.cursor()
                cursor.execute(
                    "SELECT count(*) FROM rating WHERE game_uuid = "
                    "? AND work_rating = ? AND like_rating = ? "
                    "AND updated = ?",
                    (
                        update["game"],
                        update["work"],
                        update["like"],
                        update["updated"],
                    ),
                )
                if cursor.fetchone()[0] == 1:
                    # we want to avoid needlessly creating update transactions
                    continue
                cursor.execute(
                    "DELETE FROM rating WHERE game_uuid = ?", (update["game"],)
                )
                cursor.execute(
                    "INSERT INTO rating (game_uuid, work_rating, "
                    "like_rating, updated) VALUES (?, ?, ?, ?)",
                    (
                        update["game"],
                        update["work"],
                        update["like"],
                        update["updated"],
                    ),
                )
            t2 = time.time()
            print("  {0:0.2f} seconds".format(t2 - t1))

        # print(
        #     "downloaded size: {0:0.2f} MiB".format(
        #         self.downloaded_size / (1024 * 1024)
        #     )
        # )

    def url_prefix(self):
        if self.host:
            if is_http_url(self.host):
                url_prefix = self.host
            else:
                url_prefix = "http://{}".format(self.host)
        else:
            url_prefix = openretro_url_prefix()
        return url_prefix

    def fetch_game_sync_data(self):
        last_id = self.database.get_last_game_id()
        if self.context.meta["games"][self.platform_id]["sync"] == last_id:
            print("[SYNC] Platform {} already synced".format(self.platform_id))
            return b""
        self.set_status(
            gettext("Fetching database entries ({0})").format(last_id + 1)
        )
        url = "{0}/api/sync/{1}/games?v=3&sync={2}".format(
            self.url_prefix(), self.platform_id, last_id
        )
        print(url)
        data = self.fetch_data(url)
        # self.downloaded_size += len(data)
        return data

    def fetch_rating_entries(self):
        cursor = self.client.database.cursor()
        cursor.execute("SELECT max(updated) FROM rating")
        row = cursor.fetchone()
        last_time = row[0]
        if not last_time:
            last_time = "2012-01-01 00:00:00"
        self.set_status(
            gettext("Fetching game ratings ({0})").format(last_time)
        )
        url = "{0}/api/sync/{1}/ratings?from={2}".format(
            self.url_prefix(), self.platform_id, quote_plus(last_time)
        )
        print(url)
        # data, json_data = self.fetch_json(url)
        json_data = self.fetch_json(url)
        # self.downloaded_size += len(data)
        return json_data

    def fetch_json_attempt(self, url):
        r = requests.get(url, auth=self.auth())
        r.raise_for_status()
        return r.json()

    def fetch_data_attempt(self, url):
        r = requests.get(url, auth=self.auth())
        r.raise_for_status()
        return r.content

    def fetch_json(self, url):
        for i in range(20):
            try:
                return self.fetch_json_attempt(url)
            except Exception as e:
                print(e)
                sleep_time = 2.0 + i * 0.3
                # FIXME: change second {0} to {1}
                self.set_status(
                    gettext(
                        "Download failed (attempt {0}) - "
                        "retrying in {1} seconds"
                    ).format(i + 1, int(sleep_time))
                )
                for _ in range(int(sleep_time) * 10):
                    time.sleep(0.1)
                    if self.stop_check():
                        return
                self.set_status(
                    gettext("Retrying last operation (attempt {0})").format(
                        i + 1
                    )
                )
        return self.fetch_json_attempt(url)

    def fetch_data(self, url):
        for i in range(10):
            try:
                return self.fetch_data_attempt(url)
            except Exception as e:
                print(e)
                sleep_time = 2.0 + i * 0.3
                # FIXME: change second {0} to {1}
                self.set_status(
                    gettext(
                        "Download failed (attempt {0}) - "
                        "retrying in {1} seconds"
                    ).format(i + 1, int(sleep_time))
                )
                for _ in range(int(sleep_time) * 10):
                    time.sleep(0.1)
                    if self.stop_check():
                        return
                self.set_status(
                    gettext("Retrying last operation (attempt {0})").format(
                        i + 1
                    )
                )
        return self.fetch_data_attempt(url)
