# encoding: utf-8
#
# Copyright 2017 Greg Neagle.
#
# Licensed under the Apache License, Version 2.0 (the 'License');
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an 'AS IS' BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
cliutils

Created by Greg Neagle on 2017-03-12.

Functions supporting the admin command-line tools
"""

import ctypes
from ctypes.util import find_library
import os
import plistlib
import readline
import sys
import tempfile
import thread
import time
import urllib
import urlparse
from xml.parsers.expat import ExpatError


FOUNDATION_SUPPORT = True
try:
    # PyLint cannot properly find names inside Cocoa libraries, so issues bogus
    # No name 'Foo' in module 'Bar' warnings. Disable them.
    # pylint: disable=E0611
    from Foundation import CFPreferencesAppSynchronize
    from Foundation import CFPreferencesCopyAppValue
    from Foundation import CFPreferencesSetAppValue
    # pylint: enable=E0611
except ImportError:
    # CoreFoundation/Foundation isn't available
    FOUNDATION_SUPPORT = False

BUNDLE_ID = 'com.googlecode.munki.munkiimport'
PREFSNAME = BUNDLE_ID + '.plist'
PREFSPATH = os.path.expanduser(os.path.join('~/Library/Preferences', PREFSNAME))

if FOUNDATION_SUPPORT:
    def pref(prefname):
        """Return a preference. Since this uses CFPreferencesCopyAppValue,
        Preferences can be defined several places. Precedence is:
            - MCX/Configuration Profile
            - ~/Library/Preferences/ByHost/
                com.googlecode.munki.munkiimport.XX.plist
            - ~/Library/Preferences/com.googlecode.munki.munkiimport.plist
            - /Library/Preferences/com.googlecode.munki.munkiimport.plist
        """
        return CFPreferencesCopyAppValue(prefname, BUNDLE_ID)

else:
    def pref(prefname):
        """Returns a preference for prefname. This is a fallback mechanism if
        CoreFoundation functions are not available -- for example to allow the
        possible use of makecatlogs or manifestutil on Linux"""
        if not hasattr(pref, 'cache'):
            pref.cache = None
        if not pref.cache:
            try:
                pref.cache = plistlib.readPlist(PREFSPATH)
            except (IOError, OSError, ExpatError):
                pref.cache = {}
        if prefname in pref.cache:
            return pref.cache[prefname]
        else:
            return None


def get_version():
    """Returns version of munkitools, reading version.plist"""
    # this implementation avoids calling Foundation and will work on
    # non Apple OSes.
    vers = "UNKNOWN"
    build = ""
    # find the munkilib directory, and the version file
    munkilibdir = os.path.dirname(os.path.abspath(__file__))
    versionfile = os.path.join(munkilibdir, "version.plist")
    if os.path.exists(versionfile):
        try:
            vers_plist = plistlib.readPlist(versionfile)
        except (IOError, OSError, ExpatError):
            pass
        else:
            try:
                vers = vers_plist['CFBundleShortVersionString']
                build = vers_plist['BuildNumber']
            except KeyError:
                pass
    if build:
        vers = vers + "." + build
    return vers


def path2url(path):
    '''Converts a path to a file: url'''
    return urlparse.urljoin('file:', urllib.pathname2url(
        os.path.abspath(os.path.expanduser(path))))


def print_utf8(text):
    '''Print Unicode text as UTF-8'''
    print text.encode('UTF-8')


def print_err_utf8(text):
    '''Print Unicode text to stderr as UTF-8'''
    print >> sys.stderr, text.encode('UTF-8')


class TempFile(object):
    '''A class that creates a temp file that is automatically deleted when
    the object goes out of scope.'''
    # pylint: disable=too-few-public-methods
    def __init__(self):
        filedesc, filepath = tempfile.mkstemp()
        # we just want the path; close the file descriptor
        os.close(filedesc)
        self.path = filepath

    def __del__(self):
        try:
            os.unlink(self.path)
        except OSError:
            pass


# pylint: disable=invalid-name
libedit = None
if 'libedit' in readline.__doc__:
    # readline module was compiled against libedit
    libedit = ctypes.cdll.LoadLibrary(find_library('libedit'))
# pylint: enable=invalid-name


def raw_input_with_default(prompt, default_text):
    '''Get input from user with a prompt and a suggested default value'''

    # 10.6's libedit doesn't have the rl_set_prompt function, so we fall back
    # to the previous behavior
    darwin_vers = int(os.uname()[2].split('.')[0])
    if darwin_vers == 10:
        if default_text:
            prompt = '%s [%s]: ' % (prompt.rstrip(': '), default_text)
            return (unicode(raw_input(prompt), encoding=sys.stdin.encoding) or
                    unicode(default_text))
        else:
            # no default value, just call raw_input
            return unicode(raw_input(prompt), encoding=sys.stdin.encoding)

    # A nasty, nasty hack to get around Python readline limitations under
    # OS X. Gives us editable default text for configuration and munkiimport
    # choices'''
    def insert_default_text(prompt, text):
        '''Helper function'''
        time.sleep(0.01)
        libedit.rl_set_prompt(prompt)
        readline.insert_text(text)
        libedit.rl_forced_update_display()

    readline.clear_history()
    if not default_text:
        return unicode(raw_input(prompt), encoding=sys.stdin.encoding)
    elif libedit:
        # readline module was compiled against libedit
        thread.start_new_thread(insert_default_text, (prompt, default_text))
        return unicode(raw_input(), encoding=sys.stdin.encoding)
    else:
        readline.set_startup_hook(lambda: readline.insert_text(default_text))
        try:
            return unicode(raw_input(prompt), encoding=sys.stdin.encoding)
        finally:
            readline.set_startup_hook()


class ConfigurationSaveError(Exception):
    '''Error to raise if there's an error saving configuration'''
    pass


def configure(prompt_list):
    """Gets configuration options and saves them to preferences store"""
    darwin_vers = int(os.uname()[2].split('.')[0])
    edited_prefs = {}
    for (key, prompt) in prompt_list:
        newvalue = raw_input_with_default('%15s: ' % prompt, pref(key))
        if darwin_vers == 10:
            # old behavior in SL: hitting return gives you an empty string,
            # and means accept the default value.
            edited_prefs[key] = newvalue or pref(key) or ''
        else:
            # just use the edited value as-is
            edited_prefs[key] = newvalue

    if FOUNDATION_SUPPORT:
        for key, value in edited_prefs.items():
            try:
                CFPreferencesSetAppValue(key, value, BUNDLE_ID)
            except BaseException:
                print >> sys.stderr, 'Could not save configuration!'
                raise ConfigurationSaveError
            # remove repo_path if it exists since we don't use that
            # any longer (except for backwards compatibility) and we don't
            # want it getting out of sync with the repo_url
            CFPreferencesSetAppValue('repo_path', None, BUNDLE_ID)
        CFPreferencesAppSynchronize(BUNDLE_ID)

    else:
        try:
            existing_prefs = plistlib.readPlist(PREFSPATH)
            existing_prefs.update(edited_prefs)
            # remove repo_path if it exists since we don't use that
            # any longer (except for backwards compatibility) and we don't
            # want it getting out of sync with the repo_url
            if 'repo_path' in existing_prefs:
                del existing_prefs['repo_path']
            plistlib.writePlist(existing_prefs, PREFSPATH)
        except (IOError, OSError, ExpatError):
            print >> sys.stderr, (
                'Could not save configuration to %s' % PREFSPATH)
            raise ConfigurationSaveError


if __name__ == '__main__':
    print 'This is a library of support tools for the Munki Suite.'
