###
# Copyright 2008-2011 Diamond Light Source Ltd.
# This file is part of Diffcalc.
#
# Diffcalc 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 3 of the License, or
# (at your option) any later version.
#
# Diffcalc 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 Diffcalc.  If not, see <http://www.gnu.org/licenses/>.
###

from math import pi, acos, cos, sin
from functools import wraps
import textwrap

try:
    from gda.jython.commands.InputCommands import requestInput as raw_input
    GDA = True
except ImportError:
    GDA = False
    pass  # raw_input unavailable in gda
try:
    from numpy import matrix
    from numpy.linalg import norm
except ImportError:
    from numjy import matrix
    from numjy.linalg import norm


# from http://physics.nist.gov/
h_in_eV_per_s = 4.135667516E-15
c = 299792458
TWELVEISH = c * h_in_eV_per_s  # 12.39842


SMALL = 1e-10
TORAD = pi / 180
TODEG = 180 / pi


COLOURISE_TERMINAL_OUTPUT = not GDA

def bold(s):
    if not COLOURISE_TERMINAL_OUTPUT:
        return s
    else:    
        BOLD = '\033[1m'
        END = '\033[0m'
        return BOLD + s + END


def x_rotation(th):
    return matrix(((1, 0, 0), (0, cos(th), -sin(th)), (0, sin(th), cos(th))))


def y_rotation(th):
    return matrix(((cos(th), 0, sin(th)), (0, 1, 0), (-sin(th), 0, cos(th))))


def z_rotation(th):
    return matrix(((cos(th), -sin(th), 0), (sin(th), cos(th), 0), (0, 0, 1)))


def xyz_rotation(u, angle):
    u = [list(u), [0, 0, 0], [0, 0, 0]]
    u = matrix(u) / norm(matrix(u))
    e11=u[0,0]**2+(1-u[0,0]**2)*cos(angle)
    e12=u[0,0]*u[0,1]*(1-cos(angle))-u[0,2]*sin(angle)
    e13=u[0,0]*u[0,2]*(1-cos(angle))+u[0,1]*sin(angle)
    e21=u[0,0]*u[0,1]*(1-cos(angle))+u[0,2]*sin(angle)
    e22=u[0,1]**2+(1-u[0,1]**2)*cos(angle)
    e23=u[0,1]*u[0,2]*(1-cos(angle))-u[0,0]*sin(angle)
    e31=u[0,0]*u[0,2]*(1-cos(angle))-u[0,1]*sin(angle)
    e32=u[0,1]*u[0,2]*(1-cos(angle))+u[0,0]*sin(angle)
    e33=u[0,2]**2+(1-u[0,2]**2)*cos(angle)
    return matrix([[e11,e12,e13],[e21,e22,e23],[e31,e32,e33]])


class DiffcalcException(Exception):
    """Error caused by user misuse of diffraction calculator.
    """
    def __str__(self):
        lines = []
        for msg_line in self.message.split('\n'):
            lines.append('* ' + msg_line)
        width = max(len(l) for l in lines)
        lines.insert(0, '\n\n' + '*' * width)
        lines.append('*' * width)
        return '\n'.join(lines)


class AbstractPosition(object):

    def inRadians(self):
        pos = self.clone()
        pos.changeToRadians()
        return pos

    def inDegrees(self):
        pos = self.clone()
        pos.changeToDegrees()
        return pos

    def changeToRadians(self):
        raise NotImplementedError()

    def changeToDegrees(self):
        raise NotImplementedError()

    def totuple(self):
        raise NotImplementedError()


### Matrices

def cross3(x, y):
    """z = cross3(x ,y) -- where x, y & z are 3*1 Jama matrices"""
    [[x1], [x2], [x3]] = x.tolist()
    [[y1], [y2], [y3]] = y.tolist()
    return matrix([[x2 * y3 - x3 * y2],
                   [x3 * y1 - x1 * y3],
                   [x1 * y2 - x2 * y1]])


def dot3(x, y):
    """z = dot3(x ,y) -- where x, y are 3*1 Jama matrices"""
    return x[0, 0] * y[0, 0] + x[1, 0] * y[1, 0] + x[2, 0] * y[2, 0]


def angle_between_vectors(a, b):
    costheta = dot3(a * (1 / norm(a)), b * (1 / norm(b)))
    return acos(bound(costheta))


## Math

def bound(x):
    """
    moves x between -1 and 1. Used to correct for rounding errors which may
    have moved the sin or cosine of a value outside this range.
    """
    
    SMALL = 1e-3    ###GOBBO
    if abs(x) > (1 + SMALL):
        raise AssertionError(
            "The value (%f) was unexpectedly too far outside -1 or 1 to "
            "safely bound. Please report this." % x)
    if x > 1:
        return 1
    if x < -1:
        return -1
    return x


def matrixToString(m):
    ''' str = matrixToString(m) --- displays a Jama matrix m as a string
    '''
    toReturn = ''
    for row in m.array:
        for el in row:
            toReturn += str(el) + '\t'
        toReturn += '\n'
    return toReturn


def nearlyEqual(first, second, tolerance):
    if type(first) in (int, float):
        return abs(first - second) <= tolerance

    if type(first) != type(matrix([[1]])):
        # lists
        first = matrix([list(first)])
        second = matrix([list(second)])
    diff = first - (second)
    return norm(diff) <= tolerance


def radiansEquivilant(first, second, tolerance):
    if abs(first - second) <= tolerance:
        return True
    if abs((first - 2 * pi) - second) <= tolerance:
        return True
    if abs((first + 2 * pi) - second) <= tolerance:
        return True
    if abs(first - (second - 2 * pi)) <= tolerance:
        return True
    if abs(first - (second + 2 * pi)) <= tolerance:
        return True

    return False


def degreesEquivilant(first, second, tolerance):
    return radiansEquivilant(first * TORAD, second * TORAD, tolerance)


def differ(first, second, tolerance):
    """Returns error message if the norm of the difference between two arrays
    or numbers is greater than the given tolerance. Else returns False.
    """
    # TODO: Fix spaghetti
    nonArray = False
    if type(first) in (int, float):
        if type(second) not in (int, float):
            raise TypeError(
                "If first is an int or float, so must second. "
                "first=%s, second=%s" & (repr(first), repr(second)))
        first = [first]
        second = [second]
        nonArray = True
    if not isinstance(first, matrix):
        first = matrix([list(first)])
    if not isinstance(second, matrix):
        second = matrix([list(second)])
    diff = first - second
    if norm(diff) >= tolerance:
        if nonArray:
            return ('%s!=%s' %
                    (repr(first.tolist()[0][0]), repr(second.tolist()[0][0])))
        return ('%s!=%s' %
                (repr(tuple(first.tolist()[0])),
                repr(tuple(second.tolist()[0]))))
    return False


### user input

def getInputWithDefault(prompt, default=""):
    """
    Prompts user for input and returns if possible a float or a list of floats,
    or if failing this a string.    default may be a number, array of numbers,
    or string.
    """
    if default is not "":
        # Generate default string
        if type(default) in (list, tuple):
            defaultString = ""
            for val in default:
                defaultString += str(val) + ' '
            defaultString = defaultString.strip()
        else:
            defaultString = str(default)
        prompt = str(prompt) + '[' + defaultString + ']: '
    else:
        prompt = str(prompt) + ': '

    rawresult = raw_input(prompt)

    # Return default if no input provided
    if rawresult == "":
        return default

    # Try to process result into list of numbers
    try:
        result = []
        for val in rawresult.split():
            result.append(float(val))
    except ValueError:
        # return a string
        return rawresult
    if len(result) == 1:
        result = result[0]
    return result


class MockRawInput(object):
    def __init__(self, toReturnList):
        if type(toReturnList) != list:
            toReturnList = [toReturnList]
        self.toReturnList = toReturnList

    def __call__(self, prompt):
        toReturn = self.toReturnList.pop(0)
        if type(toReturn) != str:
            raise TypeError
        print prompt + toReturn
        return toReturn


def getMessageFromException(e):
    try:  # Jython
        return e.args[0]
    except:
        try:  # Python
            return e.message
        except:
            # Java
            return e.args[0]


def promptForNumber(prompt, default=""):
    val = getInputWithDefault(prompt, default)
    if type(val) not in (float, int):
        return None
    return val


def promptForList(prompt, default=""):
    val = getInputWithDefault(prompt, default)
    if type(val) not in (list, tuple):
        return None
    return val


def isnum(o):
    return isinstance(o, (int, float))


def allnum(l):
    return not [o for o in l if not isnum(o)]


DEBUG = False



def command(f):
    """A decorator to wrap a command method or function.

    Calls to the decorated method or function are wrapped by call_command.
    """
    # TODO: remove one level of stack trace by not using wraps
    @wraps(f)
    def wrapper(*args, **kwds):
        return call_command(f, args)

    return wrapper


def call_command(f, args):

    if DEBUG:
        return f(*args)
    try:
        return f(*args)
    except TypeError:
        # NOTE: TypeErrors resulting from bugs in the core code will be
        # erroneously caught here! TODO: check depth of TypeError stack
        raise TypeError('\n\nUSAGE:\n' + f.__doc__)
    except DiffcalcException, e:
        # TODO: log and create a new one to shorten stack trace for user
        raise DiffcalcException(e.message)
