from flask import Flask, request, jsonify, send_file
from flask_cors import CORS
from typing import Union, List
import json
import os
import traceback


class Database(object):
    _instance = None

    @staticmethod
    def instance():
        if Database._instance is None:
            Database._instance = Database()
        return Database._instance

    def __init__(self):
        with open(config().classifier_results_json, "r") as f:
            self._data = json.load(f)
        self._has_pending_changes = False

    def _follow_path(self, path: List[str]):
        value = self._data
        for i in range(len(path)):
            if isinstance(value, dict):
                if path[i] not in value:
                    return {}
                value = value[path[i]]
            elif isinstance(value, list):
                if path[i].startswith("@"):
                    id = path[i][1:]
                    for x in value:
                        if isinstance(x, dict) and "id" in x and str(x["id"]) == id:
                            value = x
                            break
                else:
                    idx = int(path[i])
                    if idx < 0 or idx >= len(value):
                        return {}
                    value = value[idx]
            else:
                return {}
        return value

    def on_read(self, path: List[str]):
        log("Database.on_read(path='" + "/".join(path) + "')")
        return self._follow_path(path)

    def on_write(self, path: List[str], value: dict) -> Union[str, None]:
        log("Database.on_write(path='" + "/".join(path) + "', value='" + json.dumps(value) + "')")
        if len(path) == 0:
            return "The passed path is empty. A path must end with a key in a dictionary whose value will be set."
        parent = self._follow_path(path[:-1])
        if not isinstance(parent, dict):
            return "Only values in dictionaries can be written."
        parent[path[-1]] = value
        self._has_pending_changes = True
        return None

    def on_commit(self):
        log("Database.on_commit()")
        if self._has_pending_changes:
            log("Saving pending changes...")
            with open(config().classifier_results_json, "w") as f:
                json.dump(self._data, f, indent=2)
            self._has_pending_changes = False

    def has_pending_changes(self):
        return self._has_pending_changes

    def on_shutdown(self):
        log("Database.on_shutdown()")
        if self._has_pending_changes:
            print("WARNING: Database has not committed changes. They will be lost!")


app = Flask(__name__)
CORS(app)


def split_path(path: str) -> List[str]:
    return [x.strip() for x in path.split("/") if len(x.strip()) > 0]


@app.route("/", methods=["GET"])
def welcome():
    print("*** MAREK: welcome ************************************")
    return "Welcome to Antarstick database! This is DEMO for IEEE VIS conference."


def crash_log():
    # res = "*** MAREK: argv=" + str(sys.argv) + "\n"
    # res += "*** MAREK: working dir=" + str(os.path.abspath(os.getcwd())) + "\n"
    # for root, dirs, files in os.walk(os.getcwd()):
    #     if "antarstick-data" in os.path.abspath(root):
    #         for f in files:
    #             res += "*** MAREK: DATA FILE: " + os.path.abspath(os.path.join(root, f)) + "\n"
    # return res
    return "DISABLED"


@app.route("/db/<path:path_string>", methods=["GET"])
def get_data(path_string):
    try:
        print("*** MAREK: BEGIN get_data " + (path_string if path_string is not None else "NONE") + " ************************************")
        path = split_path(path_string)
        data = Database.instance().on_read(path)
        return jsonify(data)
    except:
        trace = traceback.format_exc()
        print("*** MAREK: !!! CRASH get_data !!!:\n" + trace + "\nCRASH LOG:\n" + crash_log())
        return "*** MAREK: !!! CRASH get_data !!!:\n" + trace + "\nCRASH LOG:\n" + crash_log(), 403
    finally:
        print("*** MAREK: END get_data ************************************")


@app.route("/db/<path:path_string>", methods=["POST"])
def set_data(path_string):
    return "All post requests are forbidden in this IEEE VIS demo.", 403
    # path = split_path(path_string)
    # data = request.get_json()
    # if data is None:
    #     print("FAILURE: set_data: A JSON body is expected.")
    #     return "A JSON body is expected for POST requests.", 415
    # error_msg = Database.instance().on_write(path, data)
    # if error_msg is not None:
    #     print("FAILURE: set_data: in write to database: " + error_msg + "; path: " + path_string)
    #     return error_msg, 406
    # return "Data has been written to database"


@app.route("/multipost", methods=["POST"])
def set_multiple_data():
    return "The multipost request is forbidden in this IEEE VIS demo.", 403
    # data = request.get_json()
    # if data is None:
    #     print("FAILURE: set_multiple_data: A JSON body is expected.")
    #     return "A JSON body is expected for POST requests.", 415
    # if not isinstance(data, list) or len(data) == 0 or not all(isinstance(x, list) and len(x) == 2 for x in data):
    #     print("FAILURE: set_data: multipost: The passed value is not a non-empty list of pairs (path, value).")
    #     return "In multipost the passed data must be a non-empty list of pairs (path, value).", 400
    # for path_and_value in data:
    #     path = split_path(path_and_value[0])
    #     if len(path) < 2 or path[0] != "db":
    #         print("FAILURE: set_multiple_data: wrong path: " + path_and_value[0])
    #         return "Wrong path to database: " + path_and_value[0] + "A path must be in form '/db/*/<dict-key>' ", 400
    #     path = path[1:]
    #     error_msg = Database.instance().on_write(path, path_and_value[1])
    #     if error_msg is not None:
    #         print("FAILURE: set_data: in write to database: " + error_msg + "; path: " + path_and_value[0])
    #         return error_msg, 406
    # return "Data has been written to database"


@app.route("/probe/pending_changes", methods=["GET"])
def has_pending_changes():
    state = Database.instance().has_pending_changes()
    return jsonify(state)


@app.route("/commit", methods=["POST"])
def commit():
    Database.instance().on_commit()
    return "Data saved to disk."


@app.route("/image/<path:path_string>", methods=["GET"])
def get_image(path_string):
    try:
        print("*** MAREK: BEGIN get_image " + (path_string if path_string is not None else "NONE") + " ************************************")
        path = split_path(path_string)
        if len(path) == 0:
            print("FAILURE: get_image: unsupported image format: " + path_string)
            return "A JPG image must be specified.", 415
        if not path[-1].lower().endswith(".jpg"):
            print("FAILURE: get_image: unsupported image format: " + path_string)
            return "Only JPG images are supported.", 415
        image_path = os.path.join(
            config().root_data_dir,
            config().location,
            config().year,
            "camera_images",
            *path
        )
        if not os.path.isfile(image_path):
            print("FAILURE: get_image: Image not found: " + path_string)
            return "Image not found: " + path_string + "\nPath to an image is '/<camera-name>/<image-file-name>.JPG'", 404
        return send_file(image_path, mimetype="image/jpg")
    except:
        trace = traceback.format_exc()
        print("*** MAREK: !!! CRASH get_image !!!:\n" + trace + "\nCRASH LOG:\n" + crash_log())
        return "*** MAREK: !!! CRASH get_image !!!:\n" + trace + "\nCRASH LOG:\n" + crash_log(), 403
    finally:
        print("*** MAREK: END get_image ************************************")


class Config:
    _config = None

    def __init__(self):
        self.location = "Keller Stream"
        self.year = "2018"
        self.root_data_dir = os.path.abspath(os.path.join(os.getcwd(), "antarstick-data"))
        if not os.path.isdir(self.root_data_dir):
            raise Exception("The passed data root directory '" + self.root_data_dir + "' is not valid data root.")
        self.data_dir = os.path.join(self.root_data_dir, self.location, self.year)
        if not os.path.isdir(self.data_dir):
            raise Exception("The data directory '" + self.data_dir + "' is not valid.")
        self.classifier_results_json = os.path.join(self.data_dir, "classifier_results.json")
        if not os.path.isfile(self.classifier_results_json):
            raise Exception("The data file does not exist: " + self.classifier_results_json)
        self.camera_images_root_dir = os.path.join(self.data_dir, "camera_images")
        if not os.path.isdir(self.camera_images_root_dir):
            raise Exception("The camera images directory '" + self.camera_images_root_dir + "' is not valid.")
        self.debug = True

    @staticmethod
    def instance():
        if Config._config is None:
            Config._config = Config()
        return Config._config


def config():
    return Config.instance()


def log(msg: str) -> None:
    if config().debug:
        print(msg)


if __name__ == '__main__':
    app.run(threaded=True, debug=True)
