# 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/.

from __future__ import absolute_import, unicode_literals

import itertools
import os
import time
import types
import xml.dom.minidom as minidom
import xml.etree.ElementTree as ET

from mozpack.copier import FileCopier
from mozpack.files import (FileFinder, PreprocessedFile)
from mozpack.manifests import InstallManifest
import mozpack.path as mozpath

from .common import CommonBackend
from ..frontend.data import (
    AndroidEclipseProjectData,
    ContextDerived,
    ContextWrapped,
)
from ..makeutil import Makefile
from ..util import ensureParentDir
from mozbuild.base import (
    ExecutionSummary,
    MachCommandConditions,
)


def pretty_print(element):
    """Return a pretty-printed XML string for an Element.
    """
    s = ET.tostring(element, 'utf-8')
    # minidom wraps element in a Document node; firstChild strips it.
    return minidom.parseString(s).firstChild.toprettyxml(indent='  ')


class AndroidEclipseBackend(CommonBackend):
    """Backend that generates Android Eclipse project files.
    """
    def __init__(self, environment):
        if not MachCommandConditions.is_android(environment):
            raise Exception(
                'The Android Eclipse backend is not available with this '
                'configuration.')

        super(AndroidEclipseBackend, self).__init__(environment)

    def summary(self):
        return ExecutionSummary(
            'AndroidEclipse backend executed in {execution_time:.2f}s\n'
            'Wrote {projects:d} Android Eclipse projects to {path:s}; '
            '{created:d} created; {updated:d} updated',
            execution_time=self._execution_time,
            projects=self._created_count + self._updated_count,
            path=mozpath.join(self.environment.topobjdir, 'android_eclipse'),
            created=self._created_count,
            updated=self._updated_count,
        )

    def consume_object(self, obj):
        """Write out Android Eclipse project files."""

        if not isinstance(obj, ContextDerived):
            return False

        if CommonBackend.consume_object(self, obj):
            # If CommonBackend acknowledged the object, we're done with it.
            return True

        # Handle the one case we care about specially.
        if isinstance(obj, ContextWrapped) and isinstance(obj.wrapped, AndroidEclipseProjectData):
            self._process_android_eclipse_project_data(obj.wrapped, obj.srcdir, obj.objdir)

        # We don't want to handle most things, so we just acknowledge all objects
        return True

    def consume_finished(self):
        """The common backend handles WebIDL and test files. We don't handle
        these, so we don't call our superclass.
        """

    def _Element_for_classpathentry(self, cpe):
        """Turn a ClassPathEntry into an XML Element, like one of:
        <classpathentry including="**/*.java" kind="src" path="preprocessed"/>
        <classpathentry including="**/*.java" excluding="org/mozilla/gecko/Excluded.java|org/mozilla/gecko/SecondExcluded.java" kind="src" path="src"/>
        <classpathentry including="**/*.java" kind="src" path="thirdparty">
            <attributes>
                <attribute name="ignore_optional_problems" value="true"/>
            </attributes>
        </classpathentry>
        """
        e = ET.Element('classpathentry')
        e.set('kind', 'src')
        e.set('including', '**/*.java')
        e.set('path', cpe.path)
        if cpe.exclude_patterns:
            e.set('excluding', '|'.join(sorted(cpe.exclude_patterns)))
        if cpe.ignore_warnings:
            attrs = ET.SubElement(e, 'attributes')
            attr = ET.SubElement(attrs, 'attribute')
            attr.set('name', 'ignore_optional_problems')
            attr.set('value', 'true')
        return e

    def _Element_for_referenced_project(self, name):
        """Turn a referenced project name into an XML Element, like:
        <classpathentry combineaccessrules="false" kind="src" path="/Fennec"/>
        """
        e = ET.Element('classpathentry')
        e.set('kind', 'src')
        e.set('combineaccessrules', 'false')
         # All project directories are in the same root; this
         # reference is absolute in the Eclipse namespace.
        e.set('path', '/' + name)
        return e

    def _Element_for_extra_jar(self, name):
        """Turn a referenced JAR name into an XML Element, like:
        <classpathentry exported="true" kind="lib" path="/Users/nalexander/Mozilla/gecko-dev/build/mobile/robocop/robotium-solo-4.3.1.jar"/>
        """
        e = ET.Element('classpathentry')
        e.set('kind', 'lib')
        e.set('exported', 'true')
        e.set('path', name)
        return e

    def _Element_for_filtered_resources(self, filtered_resources):
        """Turn a list of filtered resource arguments like
        ['1.0-projectRelativePath-matches-false-false-*org/mozilla/gecko/resources/**']
        into an XML Element, like:
        <filteredResources>
          <filter>
            <id>1393009101322</id>
            <name></name>
            <type>30</type>
            <matcher>
              <id>org.eclipse.ui.ide.multiFilter</id>
              <arguments>1.0-projectRelativePath-matches-false-false-*org/mozilla/gecko/resources/**</arguments>
            </matcher>
          </filter>
        </filteredResources>

        The id is random; the values are magic."""

        id = int(1000 * time.time())
        filteredResources = ET.Element('filteredResources')
        for arg in sorted(filtered_resources):
            e = ET.SubElement(filteredResources, 'filter')
            ET.SubElement(e, 'id').text = str(id)
            id += 1
            ET.SubElement(e, 'name')
            ET.SubElement(e, 'type').text = '30' # It's magic!
            matcher = ET.SubElement(e, 'matcher')
            ET.SubElement(matcher, 'id').text = 'org.eclipse.ui.ide.multiFilter'
            ET.SubElement(matcher, 'arguments').text = str(arg)
        return filteredResources

    def _manifest_for_project(self, srcdir, project):
        manifest = InstallManifest()

        if project.manifest:
            manifest.add_copy(mozpath.join(srcdir, project.manifest), 'AndroidManifest.xml')

        if project.res:
            manifest.add_symlink(mozpath.join(srcdir, project.res), 'res')
        else:
            # Eclipse expects a res directory no matter what, so we
            # make an empty directory if the project doesn't specify.
            res = os.path.abspath(mozpath.join(os.path.dirname(__file__),
                'templates', 'android_eclipse_empty_resource_directory'))
            manifest.add_pattern_copy(res, '.**', 'res')

        if project.assets:
            manifest.add_symlink(mozpath.join(srcdir, project.assets), 'assets')

        for cpe in project._classpathentries:
            manifest.add_symlink(mozpath.join(srcdir, cpe.srcdir), cpe.dstdir)

        # JARs and native libraries go in the same place. For now, we're adding
        # class path entries with the full path to required JAR files (which
        # makes sense for JARs in the source directory, but probably doesn't for
        # JARs in the object directory). This could be a problem because we only
        # know the contents of (a subdirectory of) libs/ after a successful
        # build and package, which is after build-backend time. At the cost of
        # some flexibility, we explicitly copy certain libraries here; if the
        # libraries aren't present -- namely, when the tree hasn't been packaged
        # -- this fails. That's by design, to avoid crashes on device caused by
        # missing native libraries.
        for src, dst in project.libs:
            manifest.add_copy(mozpath.join(srcdir, src), dst)

        return manifest

    def _process_android_eclipse_project_data(self, data, srcdir, objdir):
        # This can't be relative to the environment's topsrcdir,
        # because during testing topsrcdir is faked.
        template_directory = os.path.abspath(mozpath.join(os.path.dirname(__file__),
            'templates', 'android_eclipse'))

        project_directory = mozpath.join(self.environment.topobjdir, 'android_eclipse', data.name)
        manifest_path = mozpath.join(self.environment.topobjdir, 'android_eclipse', '%s.manifest' % data.name)

        manifest = self._manifest_for_project(srcdir, data)
        ensureParentDir(manifest_path)
        manifest.write(path=manifest_path)

        classpathentries = []
        for cpe in sorted(data._classpathentries, key=lambda x: x.path):
            e = self._Element_for_classpathentry(cpe)
            classpathentries.append(ET.tostring(e))

        for name in sorted(data.referenced_projects):
            e = self._Element_for_referenced_project(name)
            classpathentries.append(ET.tostring(e))

        for name in sorted(data.extra_jars):
            e = self._Element_for_extra_jar(mozpath.join(srcdir, name))
            classpathentries.append(ET.tostring(e))

        defines = {}
        defines['IDE_OBJDIR'] = objdir
        defines['IDE_TOPOBJDIR'] = self.environment.topobjdir
        defines['IDE_SRCDIR'] = srcdir
        defines['IDE_TOPSRCDIR'] = self.environment.topsrcdir
        defines['IDE_PROJECT_NAME'] = data.name
        defines['IDE_PACKAGE_NAME'] = data.package_name
        defines['IDE_PROJECT_DIRECTORY'] = project_directory
        defines['IDE_RELSRCDIR'] = mozpath.relpath(srcdir, self.environment.topsrcdir)
        defines['IDE_CLASSPATH_ENTRIES'] = '\n'.join('\t' + cpe for cpe in classpathentries)
        defines['IDE_RECURSIVE_MAKE_TARGETS'] = ' '.join(sorted(data.recursive_make_targets))
        # Like android.library=true
        defines['IDE_PROJECT_LIBRARY_SETTING'] = 'android.library=true' if data.is_library else ''
        # Like android.library.reference.1=FennecBrandingResources
        defines['IDE_PROJECT_LIBRARY_REFERENCES'] = '\n'.join(
            'android.library.reference.%s=%s' % (i + 1, ref)
            for i, ref in enumerate(sorted(data.included_projects)))
        if data.filtered_resources:
            filteredResources = self._Element_for_filtered_resources(data.filtered_resources)
            defines['IDE_PROJECT_FILTERED_RESOURCES'] = pretty_print(filteredResources).strip()
        else:
            defines['IDE_PROJECT_FILTERED_RESOURCES'] = ''
        defines['ANDROID_TARGET_SDK'] = self.environment.substs['ANDROID_TARGET_SDK']
        defines['MOZ_ANDROID_MIN_SDK_VERSION'] = self.environment.defines['MOZ_ANDROID_MIN_SDK_VERSION']

        copier = FileCopier()
        finder = FileFinder(template_directory)
        for input_filename, f in itertools.chain(finder.find('**'), finder.find('.**')):
            if input_filename == 'AndroidManifest.xml' and not data.is_library:
                # Main projects supply their own manifests.
                continue
            copier.add(input_filename, PreprocessedFile(
                mozpath.join(finder.base, input_filename),
                depfile_path=None,
                marker='#',
                defines=defines,
                extra_depends={mozpath.join(finder.base, input_filename)}))

        # When we re-create the build backend, we kill everything that was there.
        if os.path.isdir(project_directory):
            self._updated_count += 1
        else:
            self._created_count += 1
        copier.copy(project_directory, skip_if_older=False, remove_unaccounted=True)
