from __future__ import absolute_import, division, print_function

import os
import re
import sys
from os.path import isdir, isfile, join

from conda.compat import iteritems, PY3, text_type
from conda.utils import memoized, md5_file
import conda.config as cc
from conda.resolve import MatchSpec
from conda.cli.common import specs_from_url

from . import exceptions

try:
    import yaml
    from yaml import Loader, SafeLoader
except ImportError:
    sys.exit('Error: could not import yaml (required to read meta.yaml '
             'files of conda recipes)')

# Override the default string handling function to always return unicode
# objects (taken from StackOverflow)
def construct_yaml_str(self, node):
    return self.construct_scalar(node)
Loader.add_constructor(u'tag:yaml.org,2002:str', construct_yaml_str)
SafeLoader.add_constructor(u'tag:yaml.org,2002:str', construct_yaml_str)

from conda_build.config import config
from conda_build.utils import comma_join

def ns_cfg():
    # Remember to update the docs of any of this changes
    plat = cc.subdir
    py = config.CONDA_PY
    np = config.CONDA_NPY
    pl = config.CONDA_PERL
    assert isinstance(py, int), py
    d = dict(
        linux = plat.startswith('linux-'),
        linux32 = bool(plat == 'linux-32'),
        linux64 = bool(plat == 'linux-64'),
        osx = plat.startswith('osx-'),
        unix = plat.startswith(('linux-', 'osx-')),
        win = plat.startswith('win-'),
        win32 = bool(plat == 'win-32'),
        win64 = bool(plat == 'win-64'),
        pl = pl,
        py = py,
        py3k = bool(30 <= py < 40),
        py2k = bool(20 <= py < 30),
        py26 = bool(py == 26),
        py27 = bool(py == 27),
        py33 = bool(py == 33),
        py34 = bool(py == 34),
        py35 = bool(py == 35),
        np = np,
        os = os,
        environ = os.environ,
    )
    for machine in cc.non_x86_linux_machines:
        d[machine] = bool(plat == 'linux-%s' % machine)

    d.update(os.environ)
    return d


sel_pat = re.compile(r'(.+?)\s*(#.*)?\[(.+)\](?(2).*)$')
def select_lines(data, namespace):
    lines = []
    for i, line in enumerate(data.splitlines()):
        line = line.rstrip()
        if line.lstrip().startswith('#'):
            # Don't bother with comment only lines
            continue
        m = sel_pat.match(line)
        if m:
            cond = m.group(3)
            try:
                if eval(cond, namespace, {}):
                    lines.append(m.group(1))
            except:
                sys.exit('''\
Error: Invalid selector in meta.yaml line %d:
%s
''' % (i + 1, line))
                sys.exit(1)
            continue
        lines.append(line)
    return '\n'.join(lines) + '\n'


@memoized
def yamlize(data):
    try:
        return yaml.load(data)
    except yaml.parser.ParserError as e:
        try:
            import jinja2
            jinja2
        except ImportError:
            raise exceptions.UnableToParseMissingJinja2(original=e)
        raise exceptions.UnableToParse(original=e)


allowed_license_families = set("""
AGPL
Apache
BSD
GPL2
GPL3
LGPL
MIT
Other
PSF
Proprietary
Public-Domain
""".split())

def ensure_valid_license_family(meta):
    try:
        license_family = meta['about']['license_family']
    except KeyError:
        return
    if license_family not in allowed_license_families:
        raise RuntimeError(exceptions.indent("""about/license_family '%s' not allowed. Allowed families are %s.""" %
            (license_family, comma_join(sorted(allowed_license_families)))))

def parse(data):
    data = select_lines(data, ns_cfg())
    res = yamlize(data)
    # ensure the result is a dict
    if res is None:
        res = {}
    for field in FIELDS:
        if field not in res:
            continue
        if res[field] is None:
            res[field] = {}
        if not isinstance(res[field], dict):
            raise RuntimeError("The %s field should be a dict, not %s" % (field, res[field].__class__.__name__))
    # ensure those are lists
    for field in ('source/patches',
                  'build/entry_points', 'build/script_env',
                  'build/features', 'build/track_features',
                  'requirements/build', 'requirements/run',
                  'requirements/conflicts', 'test/requires',
                  'test/files', 'test/commands', 'test/imports'):
        section, key = field.split('/')
        if res.get(section) is None:
            res[section] = {}
        if res[section].get(key, None) is None:
            res[section][key] = []
    # ensure those are strings
    for field in ('package/version', 'build/string', 'source/svn_rev',
                  'source/git_tag', 'source/git_branch', 'source/md5',
                  'source/git_rev', 'source/path'):
        section, key = field.split('/')
        if res.get(section) is None:
            res[section] = {}
        val = res[section].get(key, '')
        if val is None:
            val = ''
        res[section][key] = text_type(val)
    ensure_valid_license_family(res)
    return sanitize(res)


def sanitize(meta):
    """
    Sanitize the meta-data to remove aliases/handle deprecation

    """
    # make a copy to avoid side-effects
    meta = dict(meta)
    sanitize_funs = [('source', _git_clean), ]
    for section, func in sanitize_funs:
        if section in meta:
            meta[section] = func(meta[section])
    return meta


def _git_clean(source_meta):
    """
    Reduce the redundancy in git specification by removing git_tag and
    git_branch.

    If one is specified, copy to git_rev.

    If more than one field is used to specified, exit
    and complain.
    """

    git_rev_tags_old = ('git_branch', 'git_tag')
    git_rev = 'git_rev'

    git_rev_tags = (git_rev,) + git_rev_tags_old

    has_rev_tags = tuple(bool(source_meta[tag]) for
                          tag in git_rev_tags)
    if sum(has_rev_tags) > 1:
        msg = "Error: mulitple git_revs:"
        msg += ', '.join("{}".format(key) for key, has in
                         zip(git_rev_tags, has_rev_tags) if has)
        sys.exit(msg)

    # make a copy of the input so we have no side-effects
    ret_meta = dict(source_meta)
    # loop over the old versions
    for key, has in zip(git_rev_tags[1:], has_rev_tags[1:]):
        # update if needed
        if has:
            ret_meta[git_rev_tags[0]] = ret_meta[key]
        # and remove
        del ret_meta[key]

    return ret_meta

# If you update this please update the example in
# conda-docs/docs/source/build.rst
FIELDS = {
    'package': ['name', 'version'],
    'source': ['fn', 'url', 'md5', 'sha1', 'sha256', 'path',
               'git_url', 'git_tag', 'git_branch', 'git_rev',
               'hg_url', 'hg_tag',
               'svn_url', 'svn_rev', 'svn_ignore_externals',
               'patches'],
    'build': ['number', 'string', 'entry_points', 'osx_is_app',
              'features', 'track_features', 'preserve_egg_dir',
              'no_link', 'binary_relocation', 'script', 'noarch_python',
              'has_prefix_files', 'binary_has_prefix_files', 'script_env',
              'detect_binary_files_with_prefix', 'rpaths',
              'always_include_files', 'skip'],
    'requirements': ['build', 'run', 'conflicts'],
    'app': ['entry', 'icon', 'summary', 'type', 'cli_opts',
            'own_environment'],
    'test': ['requires', 'commands', 'files', 'imports'],
    'about': ['home', 'license', 'license_family',
              'summary', 'readme', 'license_file'],
}


def check_bad_chrs(s, field):
    bad_chrs = '=!@#$%^&*:;"\'\\|<>?/ '
    if field in ('package/version', 'build/string'):
        bad_chrs += '-'
    for c in bad_chrs:
        if c in s:
            sys.exit("Error: bad character '%s' in %s: %s" % (c, field, s))


def get_contents(meta_path):
    '''
    Get the contents of the [meta.yaml|conda.yaml] file.
    If jinja is installed, then the template.render function is called
    before standard conda macro processors
    '''
    try:
        import jinja2
    except ImportError:
        print("There was an error importing jinja2.", file=sys.stderr)
        print("Please run `conda install jinja2` to enable jinja template support", file=sys.stderr)
        with open(meta_path) as fd:
            return fd.read()

    from conda_build.jinja_context import context_processor

    path, filename = os.path.split(meta_path)
    loaders = [jinja2.PackageLoader('conda_build'),
               jinja2.FileSystemLoader(path)
               ]
    env = jinja2.Environment(loader=jinja2.ChoiceLoader(loaders))
    env.globals.update(ns_cfg())
    env.globals.update(context_processor())

    template = env.get_or_select_template(filename)

    contents = template.render(environment=env)
    return contents


class MetaData(object):

    def __init__(self, path):
        assert isdir(path)
        self.path = path
        self.meta_path = join(path, 'meta.yaml')