# Copyright (c) 2009 ActiveState Software Inc.
# See http://www.activestate.com/activepython/license/ for licensing
# information.

"""
    pypm.common.util
    ~~~~~~~~~~~~~~~~
    
    Assorted utility code
"""

import os
import sys
from os.path import join, abspath, normpath, exists
import subprocess
from contextlib import contextmanager
import functools
import inspect
import json
import logging
import time
import socket
import calendar
import tempfile
from datetime import datetime
from fnmatch import fnmatch

from pkg_resources import Requirement, resource_filename

import pypm
from pypm.common import console
import pypm.common.cmdln_branches_cfg as cmdln
from pypm.common.zc_lockfile import LockFile

LOG = logging.getLogger(__name__)


#
# SimpleObject
#

class SimpleObject(object):
    """Object with a collection of fields only.

    The following features are supported:

      1) Automatically initialize the fields in __init__
      2) Ability to convert from another object types (with some common fields)
      3) Interoperate with sqlalchemy.orm (i.e., plain ``self.foo=value`` works)
    """

    # Supported public fields in this object
    FIELDS = []

    def __init__(self, **kwargs):
        """Initialize the object with FIELDS whose values are in ``kwargs``"""
        self.__assert_field_mapping(kwargs)
        for field in self.FIELDS:
            setattr(self, field, kwargs[field])

    @classmethod
    def create_from(cls, another, **kwargs):
        """Create from another object of different type

        Another object must be from a derived class of SimpleObject (which
        contains FIELDS)
        """
        reused_fields = {}
        for field, value in another.get_fields():
            if field in cls.FIELDS:
                reused_fields[field] = value
        reused_fields.update(kwargs)
        return cls(**reused_fields)

    #
    # Attribute helpers
    # 
    def get_fields(self):
        """Return fields as a list of (name,value)"""
        for field in self.FIELDS:
            yield field, getattr(self, field)

    def to_dict(self):
        return dict(self.get_fields())
        
    #
    # JSON serializer
    #
    def to_json(self):
        return json.dumps(self.to_dict())
    @classmethod
    def from_json(cls, json_string):
        values = json.loads(json_string)
        return cls(**remove_unicode_keys(values))
    
    #
    # - internal -
    #
    def __assert_field_mapping(self, mapping):
        """Assert that mapping.keys() == FIELDS"""
        passed_keys = set(mapping.keys())
        class_fields = set(self.FIELDS)
        
        assert passed_keys == class_fields, \
            "passed different set of keys ({0}) to {1} ({2})".format(
            passed_keys, self.__class__, class_fields)


def askyesno(question, default):
    """Ask (Y/N) type of question to the user"""
    assert isinstance(default, bool)
    
    s = '{0} ({1}/{2}) '.format(
        question,
        default and 'Y' or 'y',
        default and 'n' or 'N')

    while True:
        val = raw_input(s).strip().lower()
        
        if val == '':
            return default
        elif val in ('y', 'yes', 'ok'):
            return True
        elif val in ('n', 'no'):
            return False
    

def xjoin(*c):
    """Equivalent to normpath(abspath(join(*c)))"""
    return normpath(abspath(join(*c)))

def existing(path):
    """Return path, but assert its presence first"""
    assert isinstance(path, (str, unicode)), \
        'not of string type: %s <%s>' % (path, type(path))
    assert exists(path), 'file/directory not found: %s' % path
    return path

@contextmanager
def locked(lockfile):
    """'with' context to lock a file"""
    lock = LockFile(lockfile)
    try:
        yield
    finally:
        lock.close()
        
@contextmanager
def dlocked(directory):
    """Lock based on a directory
    
    You need this function if you do not want more than on process to be
    operating on a directory

    NOTE: On 'gila' when accessing /net/nas/.., locking is disabled. See:
    http://bugs.activestate.com/show_bug.cgi?id=83473
    """
    if socket.gethostname() == 'gila' and directory.startswith('/net/nas'):
        LOG.info('NOTE: NFS-locking is disabled on gila as it does not work')
        yield; return

    if not exists(directory):
        os.makedirs(directory)
    lockfile = join(directory, '.lock')
    with locked(lockfile):
        yield

def require_option(options, option_name):
    """
    >>> require_option('foo-bar')
    ...
    CmdlnUserError: required option, --foo-bar, is mising
    """
    option_var_name = option_name.replace('-', '_')
    if getattr(options, option_var_name) is None:
        raise cmdln.CmdlnUserError, \
            'required option, --%s, is missing' % option_name

def pypm_file(*paths):
    """Return absolute path to a file residing inside the pypm package using
    pkg_resources API"""
    return resource_filename(Requirement.parse('pypm'), join(*paths))

def get_root_dir(as_url=False):
    """This function returns the root project directory where directories
    like test/ pypm/ bin/ etc/ ... lie
    
    TODO - we should ideally use pkg_resources
    """
    root_dir_path = abspath(join(
        os.path.dirname(__file__),  # ./pypm/util/
        os.path.pardir,             # ./pypm
        os.path.pardir              # ./
    ))
    
    if as_url:
        root_dir_url = path_to_url(root_dir_path)
        return root_dir_url.split('file://', 1)[1]
    else:
        return root_dir_path

def lazyproperty(func):
    """A property decorator for lazy evaluation"""
    cache = {}
    def _get(self):
        """Return the property value from cache once it is calculated"""
        try:
            return cache[self]
        except KeyError:
            cache[self] = value = func(self)
            return value
        
    return property(_get)

def abs2rel(absolute_path):
    """Convert an absolute path to relative path assuming the topmost directory
    is the bast dir.
    
    >>> strip_abs_root('/opt/ActivePython/')
    'opt/ActivePython/'
    >>> strip_abs_root('/opt/ActivePython')
    'opt/ActivePython'
    """
    assert os.path.isabs(absolute_path), \
        '`%s` is not a absolute path' % absolute_path
    if sys.platform.startswith('win'):
        assert absolute_path[1:3] == ':\\'
        return absolute_path[3:]  # remove the DRIVE
    else:
        assert absolute_path[0] == '/'
        return absolute_path[1:]  # remove the '/'
        
def url_join(url, components):
    """Join URL components .. always with a forward slash"""
    assert type(components) is list
    assert '\\' not in url, \
        'URL is not supposed to contain backslashes. Is this windows path? '+url
    return url + '/' + '/'.join(components)
    
def path_to_url(path):
    """Convert local path to remote url
    """
    if sys.platform.startswith('win'):
        assert '/' not in path, \
            'windows path cannot contain forward slash: '+path
        drive, path = os.path.splitdrive(path)
        return url_join('file:///' + drive,
                        path.split('\\'))
    else:
        return 'file://' + abspath(path)
    
def remove_unicode_keys(dictobj):
    """Convert keys from 'unicode' to 'str' type.

    workaround for <http://bugs.python.org/issue2646>
    """
    assert isinstance(dictobj, dict)

    newdict = {}
    for key, value in dictobj.items():
        if type(key) is unicode:
            key = key.encode('utf-8')
        newdict[key] = value
    return newdict

def get_user_agent(default):
    """Return an user agent string representing PyPM
    
    Retain the default user-agent for backward-compat
    """
    return '{0} (PyPM {1.__version__})'.format(default, pypm)

class BareDateTime(unicode):
    """Wrapper around the DateTime object with our own standard string
    representation
    """

    DATE_FORMAT = "%Y-%m-%d"
    TIME_FORMAT = "%H:%M:%S"
    FORMAT = DATE_FORMAT + ' ' + TIME_FORMAT

    @classmethod
    def to_string(cls, dt):
        """Convert the datetime object `dt` to a string

        with format as defind by this class
        """
        return dt.strftime(cls.FORMAT)

    @classmethod
    def to_datetime(cls, dt_string):
        """Convert dt_string, formatted by `to_string()` method above"""
        ts = time.mktime(time.strptime(dt_string, cls.FORMAT))
        return datetime.fromtimestamp(ts)


def find_in_directory(path, filename_pattern):
    """Search for files or directories matching the given pattern under the
    given path

    Return the list of matching full paths.
    """
    matching_files = []
    for root, dirs, files in os.walk(path):
        matching_files.extend([join(root, f) for f in files+dirs
                               if fnmatch(f, filename_pattern)])
    return matching_files


@contextmanager
def tmpdir(prefix='tmp-', suffix=''):
    """__with__ context to create a temporary working directory
    
    At the end of the context, the temporary directory will be recursively
    deleted.
    
    CWD will be changed the created directory during the context.
    """
    d = tempfile.mkdtemp(suffix=suffix, prefix=prefix)
    with console.cd(d):
        yield d
    console.rm(d)
    
