import os
import sys
import json
import tempfile
import shutil
import utils
import logging
from compile_extensions import CompileExtensions
from urlparse import urlparse
from zips import UnzipUtil
from downloads import Downloader
from downloads import CurlDownloader
from utils import safe_makedirs
from utils import find_git_url
from utils import wrap


_log = logging.getLogger('cloudfoundry')


class CloudFoundryUtil(object):
    @staticmethod
    def initialize():
        # set stdout as non-buffered
        if hasattr(sys.stdout, 'fileno'):
             fileno = sys.stdout.fileno()
             tmp_fd = os.dup(fileno)
             sys.stdout.close()
             os.dup2(tmp_fd, fileno)
             os.close(tmp_fd)
             sys.stdout = os.fdopen(fileno, "w", 0)
        ctx = utils.FormattedDict()
        # Add environment variables
        for key, val in os.environ.iteritems():
            ctx[key] = wrap(val)
        # Convert JSON env variables
        ctx['VCAP_APPLICATION'] = json.loads(ctx.get('VCAP_APPLICATION',
                                             wrap('{}')))
        ctx['VCAP_SERVICES'] = json.loads(ctx.get('VCAP_SERVICES', wrap('{}')))
        # Build Pack Location
        ctx['BP_DIR'] = os.path.dirname(os.path.dirname(sys.argv[0]))
        # User's Application Files, build droplet here
        ctx['BUILD_DIR'] = sys.argv[1]
        # Cache space for the build pack
        ctx['CACHE_DIR'] = (len(sys.argv) == 3) and sys.argv[2] or None
        # Temp space
        if 'TMPDIR' not in ctx.keys():
            ctx['TMPDIR'] = tempfile.gettempdir()
        # Make sure cache & build directories exist
        if not os.path.exists(ctx['BUILD_DIR']):
            os.makedirs(ctx['BUILD_DIR'])
        if ctx['CACHE_DIR'] and not os.path.exists(ctx['CACHE_DIR']):
            os.makedirs(ctx['CACHE_DIR'])
        # Add place holder for extensions
        ctx['EXTENSIONS'] = []
        # Init Logging
        CloudFoundryUtil.init_logging(ctx)
        _log.info('CloudFoundry Initialized.')
        _log.debug("CloudFoundry Context Setup [%s]", ctx)

        # get default PHP, httpd, and nginx versions from manifest
        manifest_file = os.path.join(ctx['BP_DIR'], 'manifest.yml')
        for dependency in ["php", "nginx", "httpd"]:
            ctx = CloudFoundryUtil.update_default_version(dependency, manifest_file, ctx)

        # Git URL, if one exists
        ctx['BP_GIT_URL'] = find_git_url(ctx['BP_DIR'])
        _log.info('Build Pack Version: %s', ctx['BP_GIT_URL'])
        return ctx

    @staticmethod
    def update_default_version(dependency, manifest_file, ctx):
        compile_exts = CompileExtensions(ctx['BP_DIR'])

        exit_code, output = compile_exts.default_version_for(manifest_file, dependency)

        if exit_code == 1:
            _log.error("Error detecting %s default version: %s", dependency.upper(), output)
            raise RuntimeError("Error detecting %s default version" % dependency.upper())

        default_version_key = dependency.upper() + "_VERSION"
        download_url_key = dependency.upper() + "_DOWNLOAD_URL"
        modules_pattern_key = dependency.upper() + "_MODULES_PATTERN"

        ctx[default_version_key] = output
        ctx[download_url_key] = "/{0}/{1}/{0}_{1}.tar.gz".format(dependency, "{" + default_version_key + "}")

        if dependency != "nginx":
            ctx[modules_pattern_key] = "/{0}/{1}/{0}_{2}_{1}.tar.gz".format(dependency, "{" + default_version_key + "}", "{MODULE_NAME}")

        return ctx

    @staticmethod
    def init_logging(ctx):
        logFmt = '%(asctime)s [%(levelname)s] %(name)s - %(message)s'
        if ctx.get('BP_DEBUG', False):
            logging.basicConfig(level=logging.DEBUG, format=logFmt)
        else:
            logLevelStr = ctx.get('BP_LOG_LEVEL', 'INFO')
            logLevel = getattr(logging, logLevelStr, logging.INFO)
            logDir = os.path.join(ctx['BUILD_DIR'], '.bp', 'logs')
            safe_makedirs(logDir)
            logging.basicConfig(level=logLevel, format=logFmt,
                                filename=os.path.join(logDir, 'bp.log'))

    @staticmethod
    def load_json_config_file_from(folder, cfgFile, step=None):
        return CloudFoundryUtil.load_json_config_file(os.path.join(folder,
                                                                   cfgFile), step)

    @staticmethod
    def load_json_config_file(cfgPath, step=None):
        if os.path.exists(cfgPath):
            _log.debug("Loading config from [%s]", cfgPath)
            with open(cfgPath, 'rt') as cfgFile:
                try:
                    return json.load(cfgFile)
                except ValueError, e:
                    _log.warn("Error reading [%s]", cfgPath)
                    _log.debug("Error reading [%s]", cfgPath, exc_info=e)
                    if step != 'detect':
                        print 'Incorrectly formatted JSON object at: %s' % cfgPath
                        cfgFile.seek(0)
                        for line in cfgFile:
                            print line
                        exit(1)
        return {}


class CloudFoundryInstaller(object):
    def __init__(self, ctx):
        self._log = _log
        self._ctx = ctx
        self._unzipUtil = UnzipUtil(ctx)
        self._dwn = self._get_downloader(ctx)(ctx)

    def _get_downloader(self, ctx):
        method = ctx.get('DOWNLOAD_METHOD', 'python')
        if method == 'python':
            self._log.debug('Using python downloader.')
            return Downloader
        elif method == 'curl':
            self._log.debug('Using cURL downloader.')
            return CurlDownloader
        elif method == 'custom':
            fullClsName = ctx['DOWNLOAD_CLASS']
            self._log.debug('Using custom downloader [%s].', fullClsName)
            dotLoc = fullClsName.rfind('.')
            if dotLoc >= 0:
                clsName = fullClsName[dotLoc + 1: len(fullClsName)]
                modName = fullClsName[0:dotLoc]
                m = __import__(modName, globals(), locals(), [clsName])
                try:
                    return getattr(m, clsName)
                except AttributeError:
                    self._log.exception(
                            'WARNING: DOWNLOAD_CLASS not found!')
            else:
                self._log.error(
                        'WARNING: DOWNLOAD_CLASS invalid, must include '
                        'package name!')
                return Downloader

    def _is_url(self, val):
        return urlparse(val).scheme != ''

    def install_binary_direct(self, url, hsh, installDir,
            fileName=None, strip=False,
            extract=True):

        if not fileName:
            fileName = urlparse(url).path.split('/')[-1]
        fileToInstall = os.path.join(self._ctx['TMPDIR'], fileName)

        self._log.debug("Installing direct [%s]", url)
        self._dwn.custom_extension_download(url, url, fileToInstall)

        if extract:
            return self._unzipUtil.extract(fileToInstall,
                    installDir,
                    strip)
        else:
            shutil.copy(fileToInstall, installDir)
            return installDir

    def _install_binary_from_manifest(self, url, installDir,
            strip=False,
            extract=True):
        self._log.debug("Installing binary from manifest [%s]", url)
        self._dwn.download(url, self._ctx['TMPDIR'])

        fileName = urlparse(url).path.split('/')[-1]
        fileToInstall = os.path.join(self._ctx['TMPDIR'], fileName)

        if extract:
            return self._unzipUtil.extract(fileToInstall,
                    installDir,
                    strip)
        else:
            shutil.copy(fileToInstall, installDir)
            return installDir

    def install_binary(self, installKey):
        self._log.debug('Installing [%s]', installKey)
        url = self._ctx['%s_DOWNLOAD_URL' % installKey]

        installDir = os.path.join(self._ctx['BUILD_DIR'],
                self._ctx.get(
                    '%s_PACKAGE_INSTALL_DIR' % installKey,
                    installKey.lower()))
        strip = self._ctx.get('%s_STRIP' % installKey, False)

        return self._install_binary_from_manifest(url, installDir,
                strip=strip)

    def _install_from(self, fromPath, fromLoc, toLocation=None, ignore=None):
        """Copy file or directory from a location to the droplet

        Copies a file or directory from a location to the application
        droplet. Directories are copied recursively, but specific files
        in those directories can be ignored by specifing the ignore parameter.

            fromPath   -> file to copy, relative build pack
            fromLoc    -> root of the from path.  Full path to file or
                          directory to be copied is fromLoc + fromPath
            toLocation -> optional location where to copy the file
                          relative to app droplet.  If not specified
                          uses fromPath.
            ignore     -> an optional callable that is passed to
                          the ignore argument of shutil.copytree.
        """
        self._log.debug("Install file [%s] from [%s]", fromPath, fromLoc)
        fullPathFrom = os.path.join(fromLoc, fromPath)
        if os.path.exists(fullPathFrom):
            fullPathTo = os.path.join(
                self._ctx['BUILD_DIR'],
                ((toLocation is None) and fromPath or toLocation))
            safe_makedirs(os.path.dirname(fullPathTo))
            self._log.debug("Copying [%s] to [%s]", fullPathFrom, fullPathTo)
            if os.path.isfile(fullPathFrom):
                shutil.copy(fullPathFrom, fullPathTo)
            else:
                utils.copytree(fullPathFrom, fullPathTo, ignore=ignore)

    def install_from_build_pack(self, fromPath, toLocation=None, ignore=None):
        """Copy file or directory from the build pack to the droplet

        Copies a file or directory from the build pack to the application
        droplet. Directories are copied recursively, but specific files
        in those directories can be ignored by specifing the ignore parameter.

            fromPath   -> file to copy, relative build pack
            toLocation -> optional location where to copy the file
                          relative to app droplet.  If not specified
                          uses fromPath.
            ignore     -> an optional callable that is passed to
                          the ignore argument of shutil.copytree.
        """
        self._install_from(
            fromPath,
            self._ctx['BP_DIR'],
            toLocation,
            ignore)

    def install_from_application(self, fromPath, toLocation, ignore=None):
        """Copy file or directory from one place to another in the application

        Copies a file or directory from one place to another place within the
        application droplet.

            fromPath   -> file or directory to copy, relative
                          to application droplet.
            toLocation -> location where to copy the file,
                          relative to app droplet.
            ignore     -> optional callable that is passed to the
                          ignore argument of shutil.copytree
        """
        self._install_from(
            fromPath,
            self._ctx['BUILD_DIR'],
            toLocation,
            ignore)
