#!/usr/bin/env python

# This will (hopefully) be the code to extract symmetry operations
# from Hall symbols

import numpy as np

lattice_symbols = {
    'P': [[0, 0, 0]],
    'A': [[0, 0, 0], [0, 1./2, 1./2]],
    'B': [[0, 0, 0], [1./2, 0, 1./2]],
    'C': [[0, 0, 0], [1./2, 1./2, 0]],
    'I': [[0, 0, 0], [1./2, 1./2, 1./2]],
    'R': [[0, 0, 0], [2./3, 1./3, 1./3], [1./3, 2./3, 2./3]],
    'H': [[0, 0, 0], [2./3, 1./3, 0], [1./3, 2./3, 0]],
    'F': [[0, 0, 0], [0, 1./2, 1./2], [1./2, 0, 1./2], [1./2, 1./2, 0]]
}

rotation_matrices = {
    '1x': [[1, 0, 0],
           [0, 1, 0],
           [0, 0, 1]],
    '1y': [[1, 0, 0],
           [0, 1, 0],
           [0, 0, 1]],
    '1z': [[1, 0, 0],
           [0, 1, 0],
           [0, 0, 1]],
    '2x': [[1, 0, 0],
           [0, -1, 0],
           [0, 0, -1]],
    '2y': [[-1, 0, 0],
           [0, 1, 0],
           [0, 0, -1]],
    '2z': [[-1, 0, 0],
           [0, -1, 0],
           [0, 0, 1]],
    '3x': [[1, 0, 0],
           [0, 0, -1],
           [0, 1, -1]],
    '3y': [[-1, 0, 1],
           [0, 1, 0],
           [-1, 0, 0]],
    '3z': [[0, -1, 0],
           [1, -1, 0],
           [0, 0, 1]],
    '4x': [[1, 0, 0],
           [0, 0, -1],
           [0, 1, 0]],
    '4y': [[0, 0, 1],
           [0, 1, 0],
           [-1, 0, 0]],
    '4z': [[0, -1, 0],
           [1, 0, 0],
           [0, 0, 1]],
    '6x': [[1, 0, 0],
           [0, 1, -1],
           [0, 1, 0]],
    '6y': [[0, 0, 1],
           [0, 1, 0],
           [-1, 0, 1]],
    '6z': [[1, -1, 0],
           [1, 0, 0],
           [0, 0, 1]],
    '2px': [[-1, 0, 0],     # b-c
            [0, 0, -1],
            [0, -1, 0]],
    '2ppx': [[-1, 0, 0],    # b+c
             [0, 0, 1],
             [0, 1, 0]],
    '2py': [[0, 0, -1],     # a-c
            [0, -1, 0],
            [-1, 0, 0]],
    '2ppy': [[0, 0, 1],    # a+c
             [0, -1, 0],
             [1, 0, 0]],
    '2pz': [[0, -1, 0],     # a-b
            [-1, 0, 0],
            [0, 0, -1]],
    '2ppz': [[0, 1, 0],    # a+b
             [1, 0, 0],
             [0, 0, -1]],
    '3*': [[0, 0, 1],     # a+b+c
           [1, 0, 0],
           [0, 1, 0]]
}

translations = {
    'a': [1./2, 0, 0],
    'b': [0, 1./2, 0],
    'c': [0, 0, 1./2],
    'n': [1./2, 1./2, 1./2],
    'u': [1./4, 0, 0],
    'v': [0, 1./4, 0],
    'w': [0, 0, 1./4],
    'd': [1./4, 1./4, 1./4]
}


def read_spg_csv(filename="spg.csv"):
    hall_symbols = []
    for line in open(filename):
        data = line.split(',')
        hall_symbols.append([data[6], data[4]])
    return hall_symbols


class HallSymbol:
    def __init__(self, hall_symbol):
        self.hall_symbol = hall_symbol.split()
        self._decompose()
        self._full_operations()

    def get_LNV(self):
        return self.L, self.N, self.V

    def get_operations(self):
        return self.G_R, self.G_T

    def _full_operations(self):
        gens_R, gens_T = self._generators()

        E = np.array(rotation_matrices['1x'])
        T0 = np.zeros(3, dtype=float)
        if self.L[0] == '-':
            G_R = [E, -E]
            G_T = [T0, T0]
        else:
            G_R = [E]
            G_T = [T0]

        for r, t in zip(gens_R, gens_T):
            G2_R, G2_T = self._get_group(r, t)
            G_R, G_T = self._multiply_groups(G_R, G_T, G2_R, G2_T)

        if self.V is not None:
            G_T = self._change_of_basis(G_R, G_T)

        G_R_with_centres = []
        G_T_with_centred = []
        for t in lattice_symbols[self.L[-1]]:
            self._lattice_translation(G_R_with_centres,
                                      G_T_with_centred,
                                      G_R, G_T, t)

        self.G_R = np.array(G_R_with_centres)
        self.G_T = np.array(G_T_with_centred)

        # Make sure the first operation has no rotation.
        assert (self.G_R[0] == rotation_matrices['1x']).all()

        # In Hall numbers 212, 213, 214, the first operation has non-zero
        # translation. This translation is subtracted from all operations.
        self.G_T -= self.G_T[0]
        self.G_T -= np.rint(self.G_T)
        cond = self.G_T < -1e-3
        self.G_T[cond] += 1

    def _change_of_basis(self, G_R, G_T):
        G_T_new = []
        v = self.V.astype(float) / 12
        for r, t in zip(G_R, G_T):
            G_T_new.append(-np.dot(r, v) + t + v)
        return G_T_new

    def _lattice_translation(self, G_R, G_T, G_R0, G_T0, translation):
        for r, t in zip(G_R0, G_T0):
            G_R.append(r.copy())
            t_new = t + translation
            G_T.append(t_new)

    def _multiply_groups(self, G1_R, G1_T, G2_R, G2_T):  # G2xG1
        G_R = []
        G_T = []
        for r1, t1 in zip(G2_R, G2_T):
            for r2, t2 in zip(G1_R, G1_T):
                G_R.append(np.dot(r1, r2))
                G_T.append(np.dot(r1, t2) + t1)

        return G_R, G_T

    def _get_group(self, r, t):
        G_R = [r, ]
        G_T = [t, ]
        while not (G_R[-1] == rotation_matrices['1x']).all():
            _r = np.dot(G_R[-1], r)
            _t = np.dot(G_R[-1], t) + G_T[-1]
            G_R.append(_r)
            G_T.append(_t)

        # Bring identity in front
        _r = G_R.pop()
        _t = G_T.pop()
        G_R.insert(0, _r)
        G_T.insert(0, _t)

        return G_R, G_T

    # def _get_group(self, r, t):
    #     G_R, G_T = self._get_group_recursive([np.array(r)], [np.array(t)])
    #     r = G_R.pop()
    #     t = G_T.pop()
    #     G_R.insert(0, r)
    #     G_T.insert(0, t)
    #     return G_R, G_T

    # def _get_group_recursive(self, G_R, G_T):
    #     if not (G_R[-1] == rotation_matrices['1x']).all():
    #         r = np.dot(G_R[-1], G_R[0])
    #         t = np.dot(G_R[-1], G_T[0]) + G_T[-1]
    #         G_R.append(r)
    #         G_T.append(t)
    #         self._get_group_recursive(G_R, G_T)
    #     return G_R, G_T

    def _generators(self):
        R = []
        T = []
        for N in self.N:
            rot = np.array(rotation_matrices[N[1] + N[2]])
            if N[0] == '-':
                rot = -rot
            R.append(rot)

            trans = np.zeros(3, dtype=float)
            if N[3] is not None:
                for t in N[3]:
                    if t in ('1', '2', '3', '4', '5'):
                        trans_screw = float(t) / int(N[1])
                        if N[2] == 'x':
                            trans[0] += trans_screw
                        elif N[2] == 'y':
                            trans[1] += trans_screw
                        elif N[2] == 'z':
                            trans[2] += trans_screw
                        else:
                            raise
                    else:
                        trans += np.array(translations[t])
            T.append(trans)

        return np.array(R, dtype=int), np.array(T, dtype=float)

    def _rotation_matrix(self, str):
        pass

    # Decompose Hall symbol
    # The following methods are used by _decompose().
    def _decompose(self):
        L = self.hall_symbol.pop(0)
        N = []
        V = None
        precededN = 0
        for i, ms in enumerate(self.hall_symbol):
            if ms[0] == '(':
                V = self._change_of_basis_symbol(self.hall_symbol[i + 2])
                break
            else:
                N.append(self._matrix_symbol(ms, i, precededN))
                precededN = int(N[-1][1][0])

        self.L = L
        self.N = N
        self.V = V

    def _matrix_symbol(self, N, i, precededN):
        if N[0] == '-':
            improper = '-'
            N = N[1:]
        else:
            improper = None

        N, R, A = self._rotation(N, i, precededN)

        if len(N) > 0:
            T = self._translation(N)
        else:
            T = None

        return [improper, R, A, T]

    def _rotation(self, N, i, precededN):
        A = None
        if N[0] == '2':
            if len(N) > 1:  # 2"
                if N[1] == '=':
                    R = '2pp'
                    A = 'z'
                    N = N[2:]
            if i == 1 and A is None:
                if precededN == 2 or precededN == 4:  # 2x
                    R = '2'
                    A = 'x'
                    N = N[1:]
                elif precededN == 3 or precededN == 6:  # 2'
                    R = '2p'
                    A = 'z'
                    N = N[1:]
        elif N[0] == '3':     # 3*
            if i == 2:
                R = '3'
                A = '*'
                N = N[1:]
            elif len(N) > 1:
                if N[1] == '*':
                    R = '3'
                    A = '*'
                    N = N[2:]

        if A is None:
            R = N[0]
            N = N[1:]
            if len(N) > 0 and i == 0:
                N, A = self._principal_axis(N)
            else:
                A = 'z'

        return N, R, A

    def _principal_axis(self, N):
        if N[0] == 'x':
            return N[1:], 'x'
        if N[0] == 'y':
            return N[1:], 'y'
        return N, 'z'

    def _translation(self, N):
        T = []
        for i in range(len(N)):
            T.append(N[i])
        return T

    def _change_of_basis_symbol(self, V):
        if V[0] == '-':
            return np.array([0, 0, -1])
        else:
            return np.array([0, 0, 1])


def dump_operations(filename):
    hall_symbols = read_spg_csv(filename)
    count = 0
    print("  0       ,  /* dummy */")
    for i in range(530):
        hs = HallSymbol(hall_symbols[i][0])
        G_R, G_T = hs.get_operations()
        for j, (r, t) in enumerate(zip(G_R, G_T)):
            count += 1
            r_encode = encode_rotation(r)
            x = np.rint(t * 12).astype(int)
            t_encode = x[0] * 144 + x[1] * 12 + x[2]
            total = t_encode * 3 ** 9 + r_encode
            text = "  %-8d," % (total)
            text += " /* %4d (%3d) [" % (count, i + 1)
            text += "%2d," * 9 % tuple(decode_rotation(total % (3**9)))
            text += "%2d,%2d,%2d] */" % tuple(decode_trans(total // (3**9)))
            print(text)


def dump_operations_old(filename):
    hall_symbols = read_spg_csv(filename)
    count = 0
    for i in range(530):
        hs = HallSymbol(hall_symbols[i][0])
        G_R, G_T = hs.get_operations()
        for j, (r, t) in enumerate(zip(G_R, G_T)):
            count += 1
            text = "{%3d," % (i + 1)
            text += "%2d,%2d,%2d,%2d,%2d,%2d,%2d,%2d,%2d," % tuple(r.ravel())
            text += "%2d,%2d,%2d" % tuple((t * 12 + 0.1).astype(int))
            text += "}, /* %4d */" % count
            print(text)


# Ternary numerical system
def encode_rotation(r):
    r_sum = 0
    for i, x in enumerate(r.ravel()):
        r_sum += (x + 1) * 3**(8 - i)
    return r_sum


def decode_rotation(c):
    r = []
    for i in range(8, -1, -1):
        r.append((c % (3**(i+1))) // (3**i) - 1)
    return np.array(r)


def decode_trans(c):
    return c // 144, (c % 144) // 12, (c % 12)


def get_reference_to_operations(filename):
    hall_symbols = read_spg_csv(filename)
    count = 0
    for i in range(530):
        hs = HallSymbol(hall_symbols[i][0])
        G_R, G_T = hs.get_operations()
        print("  {%4d,%5d}, /* %3d */ " % (len(G_R), count + 1, i + 1))
        count += len(G_R)


def watch_hs(filename, number):
    print("  {   0,    0}, /*   0 */")
    num = number - 1
    hall_symbols = read_spg_csv(filename)
    hs = HallSymbol(hall_symbols[num][0])
    for char, vals in zip(('L', 'N', 'V'), hs.get_LNV()):
        print("%s: %s" % (char, vals))
    G_R, G_T = hs.get_operations()
    print(number, ":", hall_symbols[num][0], "(", len(G_R), ")")
    for i, (r, t) in enumerate(zip(G_R, G_T)):
        print("-----", i + 1, "-----")
        print(r, t)


if __name__ == '__main__':
    """

    Usage
    -----

    To watch symmetry operations of a Hall symbol,

    % python hall2operations.py --hs spg.csv 213

    To dump symmetry operations of all Hall symbols that are copied to
    spglb_database.c,

    % python hall2operations.py --dump spg.csv

    To dump address of symmetry operation data that is copied to
    spglb_database.c,

    % python hall2operations.py --reference spg.csv

    """

    from optparse import OptionParser
    parser = OptionParser()
    parser.set_defaults(watch_hs=False,
                        dump_operations=False,
                        shift=None,
                        origin=None)
    parser.add_option("--hs", dest="watch_hs",
                      action="store_true",
                      help="spg.csv [spg NUM]")
    parser.add_option("--dump", dest="is_dump",
                      action="store_true")
    parser.add_option("--reference", dest="is_reference",
                      action="store_true")

    (options, args) = parser.parse_args()

    if options.is_dump:
        dump_operations(args[0])
    if options.is_reference:
        get_reference_to_operations(args[0])
    elif options.watch_hs:
        watch_hs(args[0], int(args[1]))
