__license__ = """
NML is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

NML 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 General Public License for more details.

You should have received a copy of the GNU General Public License along
with NML; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA."""

# -*- coding: utf-8 -*-
import sys, os

def truncate_int32(value):
    """
    Truncate the given value so it can be stored in exactly 4 bytes. The sign
    will be kept. Too high or too low values will be cut off, not clamped to
    the valid range.

    @param value: The value to truncate.
    @type value: C{int}

    @return: The truncated value.
    @rtype: C{int}.
    """
    #source: http://www.tiac.net/~sw/2010/02/PureSalsa20/index.html
    return int( (value & 0x7fffFFFF) | -(value & 0x80000000) )

def check_range(value, min_value, max_value, name, pos):
    """
    Check if a value is within a certain range and raise an error if it's not.

    @param value: The value to check.
    @type value: C{int}

    @param min_value: Minimum valid value.
    @type min_value: C{int}

    @param max_value: Maximum valid value.
    @type max_value: C{int}

    @param name: Name of the variable that is being tested.
    @type name: C{basestring}

    @param pos: Position information from the variable being tested.
    @type pos: L{Position}
    """
    if not min_value <= value <= max_value:
        raise RangeError(value, min_value, max_value, name, pos)

def greatest_common_divisor(a, b):
    """
    Get the greatest common divisor of two numbers

    @param a: First number.
    @type  a: C{int}

    @param b: Second number.
    @type  b: C{int}

    @return: Greatest common divisor.
    @rtype:  C{int]
    """
    while b != 0:
        t = b
        b = a % b
        a = t
    return a

def reverse_lookup(dic, val):
    """
    Perform reverse lookup of any key that has the provided value.

    @param dic: Dictionary to perform reverse lookup on.
    @type  dic: C{dict}

    @param val: Value being searched.
    @type  val: an existing value.

    @return: A key such that C{dict[key] == val}.
    @rtype:  Type of the matching key.
    """
    for k, v in dic.iteritems():
        if v == val: return k
    raise AssertionError("Value not found in the dictionary.")

class Position(object):
    """
    Base class representing a position in a file.

    @ivar filename: Name of the file.
    @type filename: C{str}

    @ivar includes: List of file includes
    @type includes: C{list} of L{Position}
    """
    def __init__(self, filename, includes):
        self.filename = filename
        self.includes = includes

class LinePosition(Position):
    """
    Line in a file.

    @ivar line_start: Line number (starting with 1) where the position starts.
    @type line_start: C{int}
    """
    def __init__(self, filename, line_start, includes = []):
        Position.__init__(self, filename, includes)
        self.line_start = line_start

    def __str__(self):
        return '"{}", line {:d}'.format(self.filename, self.line_start)

class PixelPosition(Position):
    """
    Position of a pixel (in a file with graphics).

    @ivar xpos: Horizontal position of the pixel.
    @type xpos: C{int}

    @ivar ypos: Vertical position of the pixel.
    @type ypos: C{int}
    """
    def __init__(self, filename, xpos, ypos):
        Position.__init__(self, filename, [])
        self.xpos = xpos
        self.ypos = ypos

    def __str__(self):
        return '"{}" at [x: {:d}, y: {:d}]'.format(self.filename, self.xpos, self.ypos)

class ImageFilePosition(Position):
    """
    Generic (not position-dependant) error with an image file
    """
    def __init__(self, filename):
        Position.__init__(self, filename, [])

    def __str__(self):
        return 'Image file "{}"'.format(self.filename)

class LanguageFilePosition(Position):
    """
    Generic (not position-dependant) error with a language file.
    """
    def __init__(self, filename):
        Position.__init__(self, filename, [])

    def __str__(self):
        return 'Language file "{}"'.format(self.filename)

class ScriptError(Exception):
    def __init__(self, value, pos = None):
        self.value = value
        self.pos = pos

    def __str__(self):
        if self.pos is None:
            return self.value
        else:
            ret = str(self.pos) + ": " + self.value
            for inc in reversed(self.pos.includes):
                ret += "\nIncluded from: " + str(inc)
            return ret

class ConstError(ScriptError):
    """
    Error to denote a compile-time integer constant was expected but not found.
    """
    def __init__(self, pos = None):
        ScriptError.__init__(self, "Expected a compile-time integer constant", pos)

class RangeError(ScriptError):
    def __init__(self, value, min_value, max_value, name, pos = None):
        ScriptError.__init__(self, name + " out of range " + str(min_value) + ".." + str(max_value) + ", encountered " + str(value), pos)

class ImageError(ScriptError):
    def __init__(self, value, filename):
        ScriptError.__init__(self, value, ImageFilePosition(filename))

class OnlyOnceError(ScriptError):
    """
    An error denoting two elements in a single grf were found, where only one is allowed.
    """
    def __init__(self, typestr, pos = None):
        """
        @param typestr: Description of the type of element encountered.
        @type  typestr: C{str}

        @param pos: Position of the error, if provided.
        @type  pos: C{None} or L{Position}
        """
        ScriptError.__init__(self, "A grf may contain only one {}.".format(typestr), pos)

class OnlyOnce:
    """
    Class to enforce that certain objects / constructs appear only once.
    """
    seen = {}

    @classmethod
    def enforce(cls, obj, typestr):
        """
        If this method is called more than once for an object of the exact same
        class, an OnlyOnceError is raised.
        """
        objtype = obj.__class__
        if objtype in cls.seen:
            raise OnlyOnceError(typestr, obj.pos)
        cls.seen[objtype] = None

    @classmethod
    def clear(cls):
        cls.seen = {}

do_print_warnings = True

def disable_warnings():
    global do_print_warnings
    do_print_warnings = False

def print_warning(msg, pos = None):
    """
    Output a warning message to the user.
    """
    if not do_print_warnings:
        return
    if pos:
        msg = str(pos) + ": " + msg

    print >> sys.stderr, "\033[33m nmlc warning: " + msg + "\033[0m "

def print_error(msg):
    """
    Output an error message to the user.
    """
    print >> sys.stderr, "nmlc ERROR: " + msg

def print_dbg(indent, *args):
    """
    Output debug text.

    @param indent: Indentation to add to the output.
    @type  indent: C{int}

    @param args: Arguments to print. An additional space is printed between them.
    @type  args: C{Tuple} of C{str}
    """
    print indent * ' ' + ' '.join(str(arg) for arg in args)


_paths = set() # Paths already found to be correct at the system.

def find_file(filepath):
    """
    Verify whether L{filepath} exists. If not, try to find a similar one with a
    different case.

    @param filepath: Path to the file to open.
    @type  filepath: C{str}

    @return: Path name to a file that exists at the file system.
    @rtype:  C{str}
    """
    # Split the filepath in components (directory parts and the filename).
    drive, filepath = os.path.splitdrive(os.path.normpath(filepath))
    # 'splitdrive' above does not remove the leading / of absolute Unix paths.
    # The 'split' below splits on os.sep, which means that loop below fails for "/".
    # To prevent that, handle the leading os.sep separately.
    if filepath.startswith(os.sep):
        drive = drive + os.sep
        filepath = filepath[len(os.sep):]

    components = [] # Path stored in reverse order (filename at index[0])
    while filepath != '':
        filepath, filepart = os.path.split(filepath)
        components.append(filepart)

    # Re-build the absolute path.
    path = drive
    if path == '':
        path = os.getcwd()
    while len(components) > 0:
        comp = components.pop()
        childpath = os.path.join(path, comp)
        if childpath in _paths:
            path = childpath
            continue

        if os.access(path, os.R_OK):
            # Path is readable, compare provided path with the file system.
            entries = os.listdir(path) + [os.curdir, os.pardir]
            lcomp = comp.lower()
            matches = [entry for entry in entries if lcomp == entry.lower()]

            if len(matches) == 0:
                raise ScriptError("Path \"{}\" does not exist (even after case conversions)".format(os.path.join(path, comp)))
            elif len(matches) > 1:
                raise ScriptError("Path \"{}\" is not unique (case conversion gave {:d} solutions)".format(os.path.join(path, comp), len(matches)))

            if matches[0] != comp:
                given_path = os.path.join(path, comp)
                real_path = os.path.join(path, matches[0])
                msg = "Path \"{}\" at the file system does not match path \"{}\" given in the input (case mismatch in the last component)"
                msg = msg.format(real_path, given_path)
                print_warning(msg)
        elif os.access(path, os.X_OK):
            # Path is only accessible, cannot inspect the file system.
            matches = [comp]
        else:
            raise ScriptError("Path \"{}\" does not exist or is not accessible".format(path))

        path = os.path.join(path, matches[0])
        if len(components) > 0:
            _paths.add(path)

    return path
