#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import print_function
from __future__ import unicode_literals

#
# Copyright 2010 Glencoe Software, Inc. All rights reserved.
# Use is subject to license terms supplied in LICENSE.txt
#

"""
Module which parses an icegrid XML file for configuration settings.

see ticket:800
see ticket:2213 - Replacing Java Preferences API
"""

from builtins import object, str
import os
import time
import logging

# To avoid conflict with omero.sys
sys = __import__("sys")

import xml.dom.minidom

from xml.etree.ElementTree import (
    XML, Element, ElementTree, SubElement, Comment, tostring
)
import omero_ext.path as path
import portalocker
import json


class Environment(object):
    """
    Object to record all the various locations
    that the active configuration can come from.
    """

    def __init__(self, user_specified=None):
        self.fallback = "default"
        self.user_specified = user_specified
        self.from_os_environ = os.environ.get("OMERO_CONFIG", None)

    def is_non_default(self):
        if self.user_specified is not None:
            return self.user_specified
        elif self.from_os_environ is not None:
            return self.from_os_environ
        return None

    def set_by_user(self, value):
        self.user_specified = value

    def internal_value(self, config):
        props = config.props_to_dict(config.internal())
        return props.get(config.DEFAULT, self.fallback)

    def for_save(self, config):
        """
        In some cases the environment chosen
        should not be persisted.
        """
        if self.user_specified:
            return self.user_specified
        else:
            return self.internal_value(config)

    def for_default(self, config):
        if self.user_specified:
            return self.user_specified
        elif self.from_os_environ:
            return self.from_os_environ
        else:
            return self.internal_value(config)


class ConfigXml(object):
    """
    dict-like wrapper around the config.xml file usually stored
    in etc/grid. For a copy of the dict, use "as_map"
    """
    KEY = "omero.config.version"
    VERSION = "5.1.0"
    INTERNAL = "__ACTIVE__"
    DEFAULT = "omero.config.profile"
    IGNORE = (KEY, DEFAULT)

    def __init__(self, filename, env_config=None, exclusive=True,
                 read_only=False):
        # Logs to the class name
        self.logger = logging.getLogger(self.__class__.__name__)
        self.XML = None  # Parsed XML Element
        self.filename = filename  # Path to the file to be read and written
        self.env_config = Environment(env_config)  # Environment override
        # Whether or not an exclusive lock should be acquired
        self.exclusive = exclusive
        # Further, if saving should even be allowed.
        self.read_only = read_only
        self.save_on_close = True
        self.open_source()

        if self.exclusive:  # must be "a+"
            try:
                portalocker.lock(
                    self.lock, portalocker.LOCK_NB | portalocker.LOCK_EX)
            except portalocker.LockException:
                self.lock = None  # Prevent deleting of the file
                self.close()
                raise

        self.source.seek(0)
        text = self.source.read()
        self.source.close()

        if text:
            self.XML = XML(text)
            try:
                self.version_check()
                self.toplinks_check()
            except:
                self.close()
                raise

        # Nothing defined, so create a new tree
        if self.XML is None:
            default = self.default()
            self.XML = Element("icegrid")
            properties = SubElement(self.XML, "properties", id=self.INTERNAL)
            SubElement(properties, "property", name=self.DEFAULT,
                       value=default)
            SubElement(properties, "property", name=self.KEY,
                       value=self.VERSION)
            properties = SubElement(self.XML, "properties", id=default)
            SubElement(properties, "property", name=self.KEY,
                       value=self.VERSION)

    def open_source(self):
        self.source = None
        if not self.read_only:
            try:
                self.source = open(self.filename, "a+b")  # Open file handle
            except IOError:
                self.logger.debug("open('%s', 'a+b') failed" % self.filename)
                if not os.path.isfile(self.filename):
                    raise
                # Before we're forced to open read-only, we need to check
                # that no other configuration has been requested because
                # it will not be possible to modify the __ACTIVE__ setting
                # once it's read-only
                val = self.env_config.is_non_default()
                if val is not None:
                    raise Exception(
                        "Non-default OMERO_CONFIG on read-only: %s" % val)
            else:
                self.lock = self._open_lock()  # Open file handle for lock

        if self.source is None:
            self.lock = None
            self.exclusive = False
            self.save_on_close = False
            # Open file handle read-only
            self.source = open(self.filename, "rb")

    def _open_lock(self):
        return open("%s.lock" % self.filename, "a+")

    def _close_lock(self):
        if self.lock is not None:
            self.lock.close()
            self.lock = None
            try:
                os.remove("%s.lock" % self.filename)
            except:
                # On windows a WindowsError 32 can happen (file opened by
                # another process), ignoring
                self.logger.error("Failed to removed lock file, ignoring",
                                  exc_info=True)
                pass

    def version(self, id=None):
        if id is None:
            id = self.default()
        properties = self.properties(id)
        if properties is not None:
            for x in list(properties):
                if x.get("name") == self.KEY:
                    return x.get("value")

    def version_check(self):
        for k, v in self.properties(None, True):
            version = self.version(k)
            if version == "4.2.0":
                self.version_fix(v, version)

    def toplinks_check(self):
        for k, v in self.properties(None, True):
            version = self.version(k)
            if version == "4.2.1" and v is not None:
                for x in list(v):
                    if x.get("name") == "omero.web.ui.top_links":
                        val = x.get("value", "")
                        toplinks = json.loads(val)
                        defaultlinks = [
                            ["Data", "webindex",
                                {"title":
                                 "Browse Data via Projects, Tags etc"}],
                            ["History", "history",
                                {"title": "History"}],
                            ["Help", "https://help.openmicroscopy.org/",
                                {"target": "new", "title":
                                    "Open OMERO user guide in a new tab"}]]
                        toplinks = defaultlinks + toplinks
                        val = json.dumps(toplinks)
                        x.set("value", val)
                    if x.get("name") == self.KEY:
                        x.set("value", self.VERSION)

    def version_fix(self, props, version):
        """
        Currently we are assuming that all blocks without a 4.2.0 version
        are bogus. The configuration script when it generates an initial
        config.xml will use prefs.class to parse the existing values and
        immediately do the upgrade.
        """
        if version == "4.2.0":
            # https://trac.openmicroscopy.org/ome/ticket/2613
            # Remove any reference to the ${omero.dollar} workaround
            # then map anything of the form: ${...} to @{...}
            if props is not None:
                for x in list(props):
                    if x.get("name", "").startswith("omero.ldap"):
                        orig = x.get("value", "")
                        val = orig.replace("${omero.dollar}", "")
                        val = val.replace("${", "@{")
                        x.set("value", val)
                        self.logger.info("Upgraded 4.2.0 property:  %s => %s",
                                         orig, val)
        else:
            raise Exception("Version mismatch: %s has %s" %
                            (props.get("id"), version))

    def internal(self):
        return self.properties(self.INTERNAL)

    def properties(self, id=None, filter_internal=False):

        if self.XML is None:
            return None

        props = self.XML.findall("./properties")
        if id is None:
            rv = list()
            for x in props:
                id = x.attrib["id"]
                if filter_internal:
                    if id == self.INTERNAL:
                        continue
                rv.append((id, x))
            return rv
        for p in props:
            if "id" in p.attrib and p.attrib["id"] == id:
                return p

    def remove(self, id=None):
        if id is None:
            id = self.default()
        properties = self.properties(id)
        if properties is None:
            raise KeyError("No such configuration: %s" % id)
        self.XML.remove(properties)

    def default(self, value=None):
        if value:
            self.env_config.set_by_user(value)

        return self.env_config.for_default(self)

    def dump(self):
        prop_list = self.properties()
        for id, p in prop_list:
            props = self.props_to_dict(p)
            print("# ===> %s <===" % id)
            print(self.dict_to_text(props))

    def save(self):
        """
        Creates a fresh <icegrid> block (removing any unwanted
        intra-element whitespace) and overwrites the file on disk.
        """
        icegrid = Element("icegrid")
        comment = Comment("\n".join([
            "\n",
            "\tThis file was generated at %s by the OmeroConfig system.",
            "\tDo not edit directly but see `omero config` for details.",
            "\tThis file may be included into your IceGrid application.",
            "\n"]) % time.ctime())
        icegrid.append(comment)
        # First step is to add a new self.INTERNAL block to it
        # which has self.DEFAULT set to the current default,
        # and then copies all the values from that profile.
        default = self.env_config.for_save(self)
        internal = SubElement(icegrid, "properties", id=self.INTERNAL)
        SubElement(internal, "property", name=self.DEFAULT, value=default)
        SubElement(internal, "property", name=self.KEY, value=self.VERSION)

        to_copy = self.properties(default)
        if to_copy is not None:
            for x in list(to_copy):
                if x.get("name") != self.DEFAULT and x.get("name") != self.KEY:
                    SubElement(internal, "property", x.attrib)
        else:
            # Doesn't exist, create it
            properties = SubElement(icegrid, "properties", id=default)
            SubElement(properties, "property", name=self.KEY,
                       value=self.VERSION)

        # Now we simply reproduce all the other blocks
        prop_list = self.properties(None, True)
        for k, p in prop_list:
            self.clear_text(p)
            icegrid.append(p)

        # Now add a single extension point which will be
        # contain a parsed version of templates.xml
        SubElement(icegrid, "include", file="templates.xml")
        self.write_element(icegrid)

    def write_element(self, icegrid):
        temp_file = path.path(self.filename + ".temp")
        try:
            # Copying etree usage from ome-model
            with open(temp_file, "wb") as o:
                ElementTree(icegrid).write(o, encoding="UTF-8")
            if sys.platform == "win32":
                os.remove(self.filename)
            temp_file.rename(self.filename)
            try:
                self._close_lock()
            except:
                self.logger.error("Failed to close lock", exc_info=1)
        except Exception as e:
            try:
                temp_file.remove()
            except:
                self.logger.error("Failed to remove temp file")
            raise e

    def close(self):
        try:
            # If we didn't get an XML instance, then something has gone wrong
            # and we should exit. Similarly, if save_on_close is False, then we
            # couldn't open the file "a+"
            if self.XML is not None and self.save_on_close:
                self.save()
                self.XML = None
        finally:
            try:
                if self.source is not None:
                    self.source.close()
                    self.source = None
            finally:
                self._close_lock()

    def props_to_dict(self, c):

        if c is None:
            return {}

        rv = dict()
        props = c.findall("./property")
        for p in props:
            if "name" in p.attrib:
                rv[p.attrib["name"]] = p.attrib.get("value", "")
        return rv

    def dict_to_text(self, parsed=None):

        if parsed is None:
            return

        rv = ""
        for k, v in list(parsed.items()):
            rv += "%s=%s" % (k, v)
        return rv

    def element_to_xml(self, elem):
        string = tostring(elem, 'utf-8')
        return xml.dom.minidom.parseString(string).toprettyxml("  ", "\n",
                                                               None)

    def clear_text(self, p):
        """
        To prevent the accumulation of text outside of elements (including
        whitespace) we walk the given element and remove tail from it and it's
        children.
        """
        p.tail = ""
        p.text = ""
        for p2 in list(p):
            self.clear_text(p2)

    #
    # Map interface on the default properties element
    #
    def as_map(self):
        return self.props_to_dict(self.properties(self.default()))

    def keys(self):
        return list(self.as_map().keys())

    def __getitem__(self, key):
        return self.props_to_dict(self.properties(self.default()))[key]

    def __setitem__(self, key, value):
        default = self.default()
        props = self.properties(default)

        if props is None:
            props = SubElement(self.XML, "properties", {"id": default})
            SubElement(props, "property", name=self.KEY, value=self.VERSION)

        if not isinstance(value, str):
            value = value.decode("utf-8")

        for x in props.findall("./property"):
            if x.attrib["name"] == key:
                x.attrib["value"] = value
                return
        SubElement(props, "property", {"name": key, "value": value})

    def __delitem__(self, key):
        default = self.default()
        props = self.properties(default)
        to_remove = []
        for p in list(props):
            if p.get("name") == key:
                to_remove.append(p)
        for x in to_remove:
            props.remove(x)
