#!/usr/bin/env python3
#
# Copyright 2016 WebAssembly Community Group participants
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

import argparse
import difflib
import fnmatch
import multiprocessing
import os
import re
import shlex
import shutil
import subprocess
import sys
import threading
import time

import find_exe
from utils import Error

IS_WINDOWS = sys.platform == 'win32'
TEST_DIR = os.path.dirname(os.path.abspath(__file__))
REPO_ROOT_DIR = os.path.dirname(TEST_DIR)
OUT_DIR = os.path.join(REPO_ROOT_DIR, 'out')
DEFAULT_TIMEOUT = 120    # seconds
SLOW_TIMEOUT_MULTIPLIER = 3


# default configurations for tests
TOOLS = {
    'wat2wasm': [
        ('RUN', '%(wat2wasm)s'),
        ('ARGS', ['%(in_file)s', '-o', '%(out_dir)s/out.wasm']),
        ('VERBOSE-ARGS', ['-v']),
    ],
    'wast2json': [
        ('RUN', '%(wast2json)s'),
        ('ARGS', ['%(in_file)s', '-o', '%(out_dir)s/out.json']),
        ('VERBOSE-ARGS', ['-v']),
    ],
    'wat-desugar': [
        ('RUN', '%(wat-desugar)s'),
        ('ARGS', ['%(in_file)s']),
    ],
    'run-objdump': [
        ('RUN', '%(wat2wasm)s %(in_file)s -o %(temp_file)s.wasm'),
        ('RUN', '%(wasm-objdump)s -r -d %(temp_file)s.wasm'),
        ('VERBOSE-ARGS', ['-v']),
    ],
    'run-objdump-gen-wasm': [
        ('RUN', '%(gen_wasm_py)s %(in_file)s -o %(temp_file)s.wasm'),
        ('RUN', '%(wasm-objdump)s -r -d %(temp_file)s.wasm'),
        ('VERBOSE-ARGS', ['-v']),
    ],
    'run-objdump-spec': [
        ('RUN', '%(wast2json)s %(in_file)s -o %(temp_file)s.json'),
        # NOTE: wasm files must be passed in manually via ARGS1
        ('RUN', '%(wasm-objdump)s -r -d'),
        ('VERBOSE-ARGS', ['-v']),
    ],
    'run-roundtrip': [
        ('RUN', 'test/run-roundtrip.py'),
        ('ARGS', [
            '%(in_file)s',
            '-v',
            '--bindir=%(bindir)s',
            '--no-error-cmdline',
            '-o',
            '%(out_dir)s',
        ]),
        ('VERBOSE-ARGS', ['--print-cmd', '-v']),
    ],
    'run-interp': [
        ('RUN', '%(wat2wasm)s %(in_file)s -o %(temp_file)s.wasm'),
        ('RUN', '%(wasm-interp)s %(temp_file)s.wasm --run-all-exports'),
        ('VERBOSE-ARGS', ['--print-cmd', '-v']),
    ],
    'run-interp-wasi': [
        ('RUN', '%(wat2wasm)s %(in_file)s -o %(temp_file)s.wasm'),
        ('RUN', '%(wasm-interp)s --wasi %(temp_file)s.wasm'),
        ('VERBOSE-ARGS', ['--print-cmd', '-v']),
    ],
    'run-interp-spec': [
        ('RUN', '%(wast2json)s %(in_file)s -o %(temp_file)s.json'),
        ('RUN', '%(spectest-interp)s %(temp_file)s.json'),
        ('VERBOSE-ARGS', ['--print-cmd', '-v']),
    ],
    'run-gen-wasm': [
        ('RUN', '%(gen_wasm_py)s %(in_file)s -o %(temp_file)s.wasm'),
        ('RUN', '%(wasm-validate)s %(temp_file)s.wasm'),
        ('RUN', '%(wasm2wat)s %(temp_file)s.wasm'),
        ('VERBOSE-ARGS', ['--print-cmd', '-v']),
    ],
    'run-gen-wasm-bad': [
        ('RUN', '%(gen_wasm_py)s %(in_file)s -o %(temp_file)s.wasm'),
        ('RUN', '%(wasm-validate)s %(temp_file)s.wasm'),
        ('ERROR', '1'),
        ('RUN', '%(wasm2wat)s %(temp_file)s.wasm'),
        ('ERROR', '1'),
        ('VERBOSE-ARGS', ['--print-cmd', '-v']),
    ],
    'run-gen-wasm-interp': [
        ('RUN', '%(gen_wasm_py)s %(in_file)s -o %(temp_file)s.wasm'),
        ('RUN', '%(wasm-interp)s --run-all-exports %(temp_file)s.wasm'),
        ('VERBOSE-ARGS', ['--print-cmd', '-v']),
    ],
    'run-gen-wasm-strip': [
        ('RUN', '%(gen_wasm_py)s %(in_file)s -o %(temp_file)s.wasm'),
        ('RUN', '%(wasm-strip)s %(temp_file)s.wasm'),
        ('RUN', '%(wasm-objdump)s -h %(temp_file)s.wasm'),
        ('VERBOSE-ARGS', ['--print-cmd', '-v']),
    ],
    'run-gen-wasm-decompile': [
        ('RUN', '%(gen_wasm_py)s %(in_file)s -o %(temp_file)s.wasm'),
        ('RUN', '%(wasm-validate)s %(temp_file)s.wasm'),
        ('RUN', '%(wasm-decompile)s %(temp_file)s.wasm'),
        ('VERBOSE-ARGS', ['--print-cmd', '-v']),
    ],
    'run-opcodecnt': [
        ('RUN', '%(wat2wasm)s %(in_file)s -o %(temp_file)s.wasm'),
        ('RUN', '%(wasm-opcodecnt)s %(temp_file)s.wasm'),
        ('VERBOSE-ARGS', ['--print-cmd', '-v']),
    ],
    'run-gen-spec-js': [
        ('RUN', '%(wast2json)s %(in_file)s -o %(temp_file)s.json'),
        ('RUN', '%(gen_spec_js_py)s %(temp_file)s.json'),
        ('VERBOSE-ARGS', ['--print-cmd', '-v']),
    ],
    'run-spec-wasm2c': [
        ('RUN', 'test/run-spec-wasm2c.py'),
        ('ARGS', [
            '%(in_file)s',
            '--bindir=%(bindir)s',
            '--no-error-cmdline',
            '-o',
            '%(out_dir)s',
        ]),
        ('VERBOSE-ARGS', ['--print-cmd', '-v']),
    ],
    'run-wasm-decompile': [
        ('RUN', '%(wat2wasm)s --enable-all %(in_file)s -o %(temp_file)s.wasm'),
        ('RUN', '%(wasm-decompile)s --enable-all %(temp_file)s.wasm'),
    ]
}

# TODO(binji): Add Windows support for compiling using run-spec-wasm2c.py
if IS_WINDOWS:
    TOOLS['run-spec-wasm2c'].append(('SKIP', ''))

ROUNDTRIP_TOOLS = ('wat2wasm',)


class NoRoundtripError(Error):
    pass


def Indent(s, spaces):
    return ''.join(' ' * spaces + l for l in s.splitlines(1))


def DiffLines(expected, actual):
    def Decode(s):
        return s.decode('utf-8', 'replace')

    expected_lines = [Decode(line) for line in expected.splitlines() if line]
    actual_lines = [Decode(line) for line in actual.splitlines() if line]
    return list(
        difflib.unified_diff(expected_lines, actual_lines, fromfile='expected',
                             tofile='actual', lineterm=''))


class Cell(object):

    def __init__(self, value):
        self.value = [value]

    def Set(self, value):
        self.value[0] = value

    def Get(self):
        return self.value[0]


def SplitArgs(value):
    if isinstance(value, list):
        return value
    return shlex.split(value)


def FixPythonExecutable(args):
    """Given an argument list beginning with a `*.py` file, return one with that
         uses sys.executable as arg[0].
    """
    exe, rest = args[0], args[1:]
    if os.path.splitext(exe)[1] == '.py':
        return [sys.executable, os.path.join(REPO_ROOT_DIR, exe)] + rest
    return args


class CommandTemplate(object):

    def __init__(self, exe):
        self.args = SplitArgs(exe)
        self.verbose_args = []
        self.stdin = None
        self.expected_returncode = 0

    def AppendArgs(self, args):
        self.args += SplitArgs(args)

    def SetExpectedReturncode(self, returncode):
        self.expected_returncode = returncode

    def AppendVerboseArgs(self, args_list):
        for level, level_args in enumerate(args_list):
            while level >= len(self.verbose_args):
                self.verbose_args.append([])
            self.verbose_args[level] += SplitArgs(level_args)

    def SetStdin(self, filename):
        self.stdin = filename

    def _Format(self, cmd, variables):
        return [arg % variables for arg in cmd]

    def GetCommand(self, variables, extra_args=None, verbose_level=0):
        args = self.args[:]
        vl = 0
        while vl < verbose_level and vl < len(self.verbose_args):
            args += self.verbose_args[vl]
            vl += 1
        if extra_args:
            args += extra_args
        args = self._Format(args, variables)
        stdin = self.stdin
        if stdin:
            stdin = stdin % variables
        return Command(self, FixPythonExecutable(args), stdin)


class Command(object):

    def __init__(self, template, args, stdin):
        self.template = template
        self.args = args
        self.stdin = stdin

    def GetExpectedReturncode(self):
        return self.template.expected_returncode

    def Run(self, cwd, timeout, console_out=False, env=None):
        process = None
        is_timeout = Cell(False)

        def KillProcess(timeout=True):
            if process:
                try:
                    if IS_WINDOWS:
                        # http://stackoverflow.com/a/10830753: deleting child processes in
                        # Windows
                        subprocess.call(['taskkill', '/F', '/T', '/PID', str(process.pid)])
                    else:
                        os.killpg(os.getpgid(process.pid), 15)
                except OSError:
                    pass
            is_timeout.Set(timeout)