# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.

"""
Handles installing open webapps (https://developer.mozilla.org/en-US/docs/Apps)
to a profile. A webapp object is a dict that contains some metadata about
the webapp and must at least include a name, description and manifestURL.

Each webapp has a manifest (https://developer.mozilla.org/en-US/docs/Apps/Manifest).
Additionally there is a separate json manifest that keeps track of the installed
webapps, their manifestURLs and their permissions.
"""

from string import Template
import json
import os
import shutil

import mozfile

__all__ = ["Webapp", "WebappCollection", "WebappFormatException", "APP_STATUS_NOT_INSTALLED",
           "APP_STATUS_INSTALLED", "APP_STATUS_PRIVILEGED", "APP_STATUS_CERTIFIED"]


# from http://hg.mozilla.org/mozilla-central/file/add0b94c2c0b/caps/idl/nsIPrincipal.idl#l163
APP_STATUS_NOT_INSTALLED = 0
APP_STATUS_INSTALLED = 1
APP_STATUS_PRIVILEGED = 2
APP_STATUS_CERTIFIED = 3


class WebappFormatException(Exception):
    """thrown for invalid webapp objects"""


class Webapp(dict):
    """A webapp definition"""

    required_keys = ('name', 'description', 'manifestURL')

    def __init__(self, *args, **kwargs):
        try:
            dict.__init__(self, *args, **kwargs)
        except (TypeError, ValueError):
            raise WebappFormatException("Webapp object should be an instance of type 'dict'")
        self.validate()

    def __eq__(self, other):
        """Webapps are considered equal if they have the same name"""
        if not isinstance(other, self.__class__):
            return False
        return self['name'] == other['name']

    def __ne__(self, other):
        """Webapps are considered not equal if they have different names"""
        return not self.__eq__(other)

    def validate(self):
        # TODO some keys are required if another key has a certain value
        for key in self.required_keys:
            if key not in self:
                raise WebappFormatException("Webapp object missing required key '%s'" % key)


class WebappCollection(object):
    """A list-like object that collects webapps and updates the webapp manifests"""

    json_template = Template(""""$name": {
  "name": "$name",
  "origin": "$origin",
  "installOrigin": "$origin",
  "receipt": null,
  "installTime": 132333986000,
  "manifestURL": "$manifestURL",
  "localId": $localId,
  "id": "$name",
  "appStatus": $appStatus,
  "csp": "$csp"
}""")

    manifest_template = Template("""{
  "name": "$name",
  "csp": "$csp",
  "description": "$description",
  "launch_path": "/",
  "developer": {
    "name": "Mozilla",
    "url": "https://mozilla.org/"
  },
  "permissions": [
  ],
  "locales": {
    "en-US": {
      "name": "$name",
      "description": "$description"
    }
  },
  "default_locale": "en-US",
  "icons": {
  }
}
""")

    def __init__(self, profile, apps=None, json_template=None, manifest_template=None):
        """
        :param profile: the file path to a profile
        :param apps: [optional] a list of webapp objects or file paths to json files describing
          webapps
        :param json_template: [optional] string template describing the webapp json format
        :param manifest_template: [optional] string template describing the webapp manifest format
        """
        if not isinstance(profile, basestring):
            raise TypeError("Must provide path to a profile, received '%s'" % type(profile))
        self.profile = profile
        self.webapps_dir = os.path.join(self.profile, 'webapps')
        self.backup_dir = os.path.join(self.profile, '.mozprofile_backup', 'webapps')

        self._apps = []
        self._installed_apps = []
        if apps:
            if not isinstance(apps, (list, set, tuple)):
                apps = [apps]

            for app in apps:
                if isinstance(app, basestring) and os.path.isfile(app):
                    self.extend(self.read_json(app))
                else:
                    self.append(app)

        self.json_template = json_template or self.json_template
        self.manifest_template = manifest_template or self.manifest_template

    def __getitem__(self, index):
        return self._apps.__getitem__(index)

    def __setitem__(self, index, value):
        return self._apps.__setitem__(index, Webapp(value))

    def __delitem__(self, index):
        return self._apps.__delitem__(index)

    def __len__(self):
        return self._apps.__len__()

    def __contains__(self, value):
        return self._apps.__contains__(Webapp(value))

    def append(self, value):
        return self._apps.append(Webapp(value))

    def insert(self, index, value):
        return self._apps.insert(index, Webapp(value))

    def extend(self, values):
        return self._apps.extend([Webapp(v) for v in values])

    def remove(self, value):
        return self._apps.remove(Webapp(value))

    def _write_webapps_json(self, apps):
        contents = []
        for app in apps:
            contents.append(self.json_template.substitute(app))
        contents = '{\n' + ',\n'.join(contents) + '\n}\n'
        webapps_json_path = os.path.join(self.webapps_dir, 'webapps.json')
        webapps_json_file = open(webapps_json_path, "w")
        webapps_json_file.write(contents)
        webapps_json_file.close()

    def _write_webapp_manifests(self, write_apps=[], remove_apps=[]):
        # Write manifests for installed apps
        for app in write_apps:
            manifest_dir = os.path.join(self.webapps_dir, app['name'])
            manifest_path = os.path.join(manifest_dir, 'manifest.webapp')
            if not os.path.isfile(manifest_path):
                if not os.path.isdir(manifest_dir):
                    os.mkdir(manifest_dir)
                manifest = self.manifest_template.substitute(app)
                manifest_file = open(manifest_path, "a")
                manifest_file.write(manifest)
                manifest_file.close()
        # Remove manifests for removed apps
        for app in remove_apps:
            self._installed_apps.remove(app)
            manifest_dir = os.path.join(self.webapps_dir, app['name'])
            mozfile.remove(manifest_dir)

    def update_manifests(self):
        """Updates the webapp manifests with the webapps represented in this collection

        If update_manifests is called a subsequent time, there could have been apps added or
        removed to the collection in the interim. The manifests will be adjusted accordingly
        """
        apps_to_install = [app for app in self._apps if app not in self._installed_apps]
        apps_to_remove = [app for app in self._installed_apps if app not in self._apps]
        if apps_to_install == apps_to_remove == []:
            # nothing to do
            return

        if not os.path.isdir(self.webapps_dir):
            os.makedirs(self.webapps_dir)
        elif not self._installed_apps:
            shutil.copytree(self.webapps_dir, self.backup_dir)

        webapps_json_path = os.path.join(self.webapps_dir, 'webapps.json')
        webapps_json = []
        if os.path.isfile(webapps_json_path):
            webapps_json = self.read_json(webapps_json_path, description="description")
            webapps_json = [a for a in webapps_json if a not in apps_to_remove]

        # Iterate over apps already in webapps.json to determine the starting local
        # id and to ensure apps are properly formatted
        start_id = 1
        for local_id, app in enumerate(webapps_json):
            app['localId'] = local_id + 1
            start_id += 1
            if not app.get('csp'):
                app['csp'] = ''
            if not app.get('appStatus'):
                app['appStatus'] = 3

        # Append apps_to_install to the pre-existent apps
        for local_id, app in enumerate(apps_to_install):
            app['localId'] = local_id + start_id
            # ignore if it's already installed
            if app in webapps_json:
                start_id -= 1
                continue
            webapps_json.append(app)
            self._installed_apps.append(app)

        # Write the full contents to webapps.json
        self._write_webapps_json(webapps_json)

        # Create/remove manifest file for each app.
        self._write_webapp_manifests(apps_to_install, apps_to_remove)

    def clean(self):
        """Remove all webapps that were installed and restore profile to previous state"""
        if self._installed_apps:
            mozfile.remove(self.webapps_dir)

        if os.path.isdir(self.backup_dir):
            shutil.copytree(self.backup_dir, self.webapps_dir)
            mozfile.remove(self.backup_dir)

        self._apps = []
        self._installed_apps = []

    @classmethod
    def read_json(cls, path, **defaults):
        """Reads a json file which describes a set of webapps. The json format is either a
        dictionary where each key represents the name of a webapp (e.g B2G format) or a list
        of webapp objects.

        :param path: Path to a json file defining webapps
        :param defaults: Default key value pairs added to each webapp object if key doesn't exist

        Returns a list of Webapp objects
        """
        f = open(path, 'r')
        app_json = json.load(f)
        f.close()

        apps = []
        if isinstance(app_json, dict):
            for k, v in app_json.iteritems():
                v['name'] = k
                apps.append(v)
        else:
            apps = app_json
            if not isinstance(apps, list):
                apps = [apps]

        ret = []
        for app in apps:
            d = defaults.copy()
            d.update(app)
            ret.append(Webapp(**d))
        return ret
