# Copyright (C) 2016 Tim Diels <timdiels.m@gmail.com>
# 
# This file is part of Chicken Turtle Project.
# 
# Chicken Turtle is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# Chicken Turtle is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
# 
# You should have received a copy of the GNU Lesser General Public License
# along with Chicken Turtle.  If not, see <http://www.gnu.org/licenses/>.

from signal import signal, SIGPIPE, SIG_DFL
from contextlib import contextmanager
from chicken_turtle_util.exceptions import UserException
from urllib.parse import urlparse, parse_qs
from pathlib import Path
from glob import glob
import plumbum as pb
import shutil
import git
import sys
import re

import logging
logger = logging.getLogger(__name__)

def eval_file(path): #TODO move into chicken turtle util
    with path.open() as f:
        return eval_string(f.read(), str(path))

def eval_string(string, name='in_memory_string'):
    code = compile(string, 'in memory string', 'exec')
    locals_ = {}
    exec(code, None, locals_)
    return locals_
    
def get_project(project_root):
    '''
    Get and validate project info from project.py
    
    Returns
    -------
    dict
        project info.
    '''
    # Load 
    try:
        project = eval_file(project_root / 'project.py')['project']
    except IOError:
        raise UserException('Must run from the directory which contains project.py')
    except KeyError:
        raise UserException('project.py must export a `project` variable (with a dict)')
    
    required_attributes = {'name', 'human_friendly_name', 'readme_file', 'description', 'author', 'author_email', 'url', 'license', 'classifiers', 'keywords', 'download_url', 'index_production'}
    optional_attributes = {'entry_points', 'index_test'}
    
    # Attributes that must be present    
    for attr in required_attributes:
        if attr not in project:
            raise UserException('Missing required attribute: project["{}"]'.format(attr))
        
    pkg_root = project_root / project['name']

    # Attributes that should not be present
    if 'version' in project:
        raise UserException('Encountered `version` in `project`. Version should be specified in {}/version.py as `__version__=...`, not in project.py'.format(pkg_root))
    elif 'package_data' in project:
        raise UserException('Encountered `package_data` in `project`. This is auto-generated, remove it. If your data directories are named `data`, have no `__init__.py` and are part of a package in {}, they will be included by auto generation.'.format(pkg_root))
    elif 'packages' in project:
        raise UserException('Encountered `packages` in `project`. This is auto-generated by mksetup, remove it.')
    elif 'long_description' in project:
        raise UserException('Encountered `long_description` in `project`. This is auto-generated by mksetup, remove it.')
    elif 'extras_require' in project:
        raise UserException('Encountered `extras_require` in `project`. Not currently supported, remove it.')
    elif 'install_requires' in project:
        raise UserException('Encountered `install_requires` in `project`. Specify these in requirements.in instead.')
    else:
        for unknown_attr in set(project.keys()) - required_attributes - optional_attributes:
            raise UserException('Encountered unknown attribute `{}` in `project`. Please remove it.'.format(unknown_attr))
        
    # Ensure values are non-empty
    for attr in project:
        if not project[attr]:
            raise UserException('Attribute `{}` may not be None'.format(attr))
        if attr != 'entry_points':
            project[attr] = project[attr].strip()
            if not project[attr]:
                raise UserException('Attribute `{}` may not be empty or whitespace'.format(attr))
        
    # Validate project name
    if re.search('\s', project['name']):
        raise UserException('Attribute `name` may not contain whitespace, use dashes instead')
    if re.search('_', project['name']):
        logger.warning('Attribute `name` contains underscores, dashes are preferred')
    if project['name'].lower() != project['name']:
        logger.warning('Attribute `name` contains upper case characters, all lower case is preferred')
    
    # Validate readme_file
    if not re.fullmatch('(.*/)?README.[a-z0-9]+', project['readme_file']):
        raise UserException('Attribute `readme_file` must be a path with as file name "README.*", case sensitive with a lower case extension')
            
    return project
    
def init_logging(debug=False):
    if debug:
        level = logging.DEBUG
    else:
        level = logging.INFO
    logging.basicConfig(level=level)
    
@contextmanager
def graceful_main(logger):
    signal(SIGPIPE, SIG_DFL)  # Ignore SIGPIPE, http://stackoverflow.com/a/30091579/1031434
    try:
        yield
    except UserException as ex:
        logger.error(ex.message)
        sys.exit(1)
    except Exception as ex:
        logger.exception(ex)
        sys.exit(2)

def get_repo(project_root):
    return git.Repo(pb.local.env.get('GIT_DIR', str(project_root)))

def parse_requirements(lines):
    for line in lines:
        match = re.fullmatch(r'(-e\s)?\s*([^#\s][^\s]*)?\s*(#.*)?', line.strip())
        result = [bool(match.group(1)), None, None, match.group(0)]
        dependency = match.group(2)
        if dependency:
            parts = re.split('(==|<|>)', dependency)
            if len(parts) > 1:
                result[1] = ''.join(parts[:-2])
                result[2] = ''.join(parts[-2:])
            else:
                result[1] = dependency 
        yield tuple(result)
    
# XXX requirements.txt files are very complex https://pip.pypa.io/en/stable/reference/pip_install/#requirements-file-format
# We do not want to duplicate the effort into parsing them
# Yet we do want to get the names of the packages that would be installed by `pip -r requirement.txt` 
# The returned url isn't even necessarily a url.
# The current limitation is very limited but seems to do alright on requirements.txt files generated by pip-compile
def parse_requirements_file(path):
    '''
    Parse requirements.txt or requirements.in file
    
    Note: requirements-parser 0.1.0 does not support -e lines it seems, hence this ad-hoc parser
    
    Parameters
    ----------
    path : pathlib.Path
        path to requirements file
        
    Returns
    -------
    Generator that yields (editable : bool, dependency_url : str, version_spec : str, whole line : str)
    '''
    # XXX return namedtuple instead
    with path.open('r') as f:
        # Ad-hoc parse each line into a dependency (requirements-parser 0.1.0 does not support -e lines it seems)
        for parsed in parse_requirements(f.readlines()):
            yield parsed
                
def path_stem_deep(path):#XXX unused, but maybe useful
    '''path name without any suffixes'''
    i = path.name.find('.')
    if i>=0:
        return path.name[:i]
    else:
        return path.name
    
def get_url_name(url):#XXX unused
    assert url
    result = urlparse(url)
    return path_stem_deep(Path(result.netloc + '/' + result.path))

def get_dependency_name(editable, url):
    if not editable:
        name = url
    else:
        match = re.search(r'egg=([^&]+)', url)
        if match:
            name = match.group(1)
        else:
            url = urlparse(url)
            name = pb.local['python'](str(Path(url.path) / 'setup.py'), '--name').strip()
        
    return name.lower().replace('_', '-')

def get_pkg_root(project_root, project_name):
    return project_root / project_name.replace('-', '_')
    
#: lower-case names of known SIP packages mapped to a sentinel package to detect whether they're installed
sip_packages = {
    'sip' : 'sip',
    'pyqt5' : 'PyQt5'
}

def is_sip_dependency(name):
    return name.lower() in sip_packages.keys()

def get_dependency_file_paths(project_root):
    paths = [Path(x) for x in glob(str(project_root / '*requirements.in'))]
    assert paths
    return paths

def remove_file(path): #TODO move to CTU: path.remove()
    '''
    Remove file or directory (recursively)
    
    Parameters
    ----------
    path : Path
    '''
    if not path.exists():
        return
    elif path.is_dir():
        shutil.rmtree(str(path))
    else:
        path.unlink()
        