# Copyright (C) 2016 the V8 project authors. All rights reserved.
# This code is governed by the BSD license found in the LICENSE file.

import os, re
import codecs, yaml
from collections import OrderedDict

from .util.find_comments import find_comments
from .util.parse_yaml import parse_yaml
from .test import Test

indentPattern = re.compile(r'^(\s*)')
interpolatePattern = re.compile(r'\{\s*(\S+)\s*\}')

def indent(text, prefix = '    ', js_value = False):
    '''Prefix a block of text (as defined by the "line break" control
    character) with some character sequence.

    :param prefix: String value to insert before each line
    :param js_value: If True, the text will be interpreted as a JavaScript
        value, meaning that indentation will not occur for lines that would
        effect the runtime value; defaults to False
    '''

    if isinstance(text, list):
        lines = text
    else:
        lines = text.split('\n')

    indented = [prefix + lines[0]]
    str_char = None

    for line in lines[1:]:
        # Determine if the beginning of the current line is part of some
        # previously-opened literal value.
        if js_value:
            for char in indented[-1]:
                if char == str_char:
                    str_char = None
                elif str_char is None and char in '\'"`':
                    str_char = char

        # Do not indent the current line if it is a continuation of a literal
        # value or if it is empty.
        if str_char or len(line) == 0:
            indented.append(line)
        else:
            indented.append(prefix + line)

    return '\n'.join(indented)

class Template:
    def __init__(self, filename, encoding):
        self.filename = filename

        with codecs.open(filename, 'r', encoding) as template_file:
            self.source = template_file.read()

        self.attribs = dict()
        self.regions = []

        self._parse()

    def _remove_comment(self, comment):
        '''Create a region that is not intended to be referenced by any case,
        ensuring that the comment is not emitted in the rendered file.'''
        name = '__remove_comment_' + str(comment['firstchar']) + '__'

        # When a removed comment ends the line, the following newline character
        # should also be removed from the generated file.
        lastchar = comment['lastchar']
        if self.source[lastchar] == '\n':
            comment['lastchar'] = comment['lastchar'] + 1

        self.regions.insert(0, dict(name=name, **comment))

    def _parse(self):
        for comment in find_comments(self.source):
            meta = parse_yaml(comment['source'])

            # Do not emit the template's frontmatter in generated files
            # (file-specific frontmatter is generated as part of the rendering
            # process)
            if meta:
                self.attribs['meta'] = meta
                self._remove_comment(comment)
                continue

            # Do not emit license information in generated files (recognized as
            # comments preceeding the YAML frontmatter)
            if not self.attribs.get('meta'):
                self._remove_comment(comment)
                continue

            match = interpolatePattern.match(comment['source'])

            if match == None:
                continue

            self.regions.insert(0, dict(name=match.group(1), **comment))

    def expand_regions(self, source, context):
        lines = source.split('\n')

        for region in self.regions:
            whitespace = indentPattern.match(lines[region['lineno']]).group(1)
            value = context['regions'].get(region['name'], '')

            str_char = region.get('in_string')
            if str_char:
                safe_char = '"' if str_char == '\'' else '\''
                value = value.replace(str_char, safe_char)
                value = value.replace('\n', '\\\n')

            source = source[:region['firstchar']] + \
                indent(value, whitespace, True).lstrip() + \
                source[region['lastchar']:]

        setup = context['regions'].get('setup')

        if setup:
            source = setup + '\n' + source

        teardown = context['regions'].get('teardown')

        if teardown:
            source += '\n' + teardown + '\n'

        return source

    def _frontmatter(self, case_filename, case_values):
        description = case_values['meta']['desc'].strip() + \
            ' (' + self.attribs['meta']['name'].strip() + ')'
        lines = []

        lines += [
            '// This file was procedurally generated from the following sources:',
            '// - ' + case_filename,
            '// - ' + self.filename,
            '/*---',
            'description: ' + description,
        ]

        esid = self.attribs['meta'].get('esid')
        if esid:
            lines.append('esid: ' + esid)

        es6id = self.attribs['meta'].get('es6id')
        if es6id:
            lines.append('es6id: ' + es6id)

        features = []
        features += case_values['meta'].get('features', [])
        features += self.attribs['meta'].get('features', [])
        features = list(OrderedDict.fromkeys(features))
        if len(features):
            lines += ['features: ' + re.sub('\n\s*', ' ', yaml.dump(features, default_flow_style=True).strip())]

        # Reconcile "negative" meta data before "flags"
        if case_values['meta'].get('negative'):
            if self.attribs['meta'].get('negative'):
                raise Exception('Cannot specify negative in case and template file')
            negative = case_values['meta'].get('negative')
        else:
            negative = self.attribs['meta'].get('negative')

        flags = ['generated']
        flags += case_values['meta'].get('flags', [])
        flags += self.attribs['meta'].get('flags', [])
        flags = list(OrderedDict.fromkeys(flags))
        if 'async' in flags and negative and negative.get('phase') == 'parse' and negative.get('type') == 'SyntaxError':
            flags.remove('async')
        lines += ['flags: ' + re.sub('\n\s*', ' ', yaml.dump(flags, default_flow_style=True).strip())]

        includes = []
        includes += case_values['meta'].get('includes', [])
        includes += self.attribs['meta'].get('includes', [])
        includes = list(OrderedDict.fromkeys(includes))
        if len(includes):
            lines += ['includes: ' + re.sub('\n\s*', ' ', yaml.dump(includes, default_flow_style=True).strip())]

        if negative:
            lines += ['negative:']
            as_yaml = yaml.dump(negative, default_flow_style=False)
            lines += indent(as_yaml.strip(), '  ').split('\n')

        info = []

        if 'info' in self.attribs['meta']:
            info.append(indent(self.attribs['meta']['info']))
        if 'info' in case_values['meta']:
            if len(info):
                info.append('')
            info.append(indent(case_values['meta']['info']))

        if len(info):
            lines.append('info: |')
            lines += info

        lines.append('---*/')

        return '\n'.join(lines)

    def expand(self, case_filename, case_name, case_values, encoding):
        frontmatter = self._frontmatter(case_filename, case_values)
        body = self.expand_regions(self.source, case_values)

        assert encoding == 'utf-8'
        return Test(self.attribs['meta']['path'] + case_name + '.js',
            source=codecs.encode(frontmatter + '\n' + body, encoding))
