#!/usr/bin/env python2
#
# Copyright 2015 Free Software Foundation, Inc.
#
# This file is part of GNU Radio
#
# GNU Radio 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, or (at your option)
# any later version.
#
# GNU Radio 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 GNU Radio; see the file COPYING.  If not, write to
# the Free Software Foundation, Inc., 51 Franklin Street,
# Boston, MA 02110-1301, USA.
#

import string, sys
from numpy import *
from numpy.random import shuffle, randint
from numpy.linalg import inv, det

# 0 gives no debug output, 1 gives a little, 2 gives a lot
#verbose = 1 #######################################################

def read_alist_file(filename):
  """
  This function reads in an alist file and creates the
  corresponding parity check matrix H. The format of alist
  files is described at:
  http://www.inference.phy.cam.ac.uk/mackay/codes/alist.html
  """

  myfile    = open(filename,'r')
  data      = myfile.readlines()
  size      = string.split(data[0])
  numCols   = int(size[0])
  numRows   = int(size[1])
  H = zeros((numRows,numCols))
  for lineNumber in arange(4,4+numCols):
    indices = string.split(data[lineNumber])
    for index in indices:
      H[int(index)-1,lineNumber-4] = 1
  # The subsequent lines in the file list the indices for where
  # the 1s are in the rows, but this is redundant
  # information.

  return H

def write_alist_file(filename, H, verbose=0):
  """
  This function writes an alist file for the parity check
  matrix. The format of alist files is desribed at:
  http://www.inference.phy.cam.ac.uk/mackay/codes/alist.html
  """

  try:
    myfile = open(filename,'w')
  except:
    sys.stderr.write("Could not open output file '{0}'".format(filename))
    sys.exit(1)

  numRows = H.shape[0]
  numCols = H.shape[1]

  tempstring = `numCols` + ' ' + `numRows` + '\n'
  myfile.write(tempstring)

  tempstring1 = ''
  tempstring2 = ''
  maxRowWeight = 0
  for rowNum in arange(numRows):
    nonzeros = array(H[rowNum,:].nonzero())
    rowWeight = nonzeros.shape[1]
    if rowWeight > maxRowWeight:
      maxRowWeight = rowWeight
    tempstring1 = tempstring1 + `rowWeight` + ' '
    for tempArray in nonzeros:
      for index in tempArray:
        tempstring2 = tempstring2 + `index+1` + ' '
      tempstring2 = tempstring2 + '\n'
  tempstring1 = tempstring1 + '\n'

  tempstring3 = ''
  tempstring4 = ''
  maxColWeight = 0
  for colNum in arange(numCols):
    nonzeros = array(H[:,colNum].nonzero())
    colWeight = nonzeros.shape[1]
    if colWeight > maxColWeight:
      maxColWeight = colWeight
    tempstring3 = tempstring3 + `colWeight` + ' '
    for tempArray in nonzeros:
      for index in tempArray:
        tempstring4 = tempstring4 + `index+1` + ' '
      tempstring4 = tempstring4 + '\n'
  tempstring3 = tempstring3 + '\n'

  tempstring = `maxColWeight` + ' ' + `maxRowWeight` + '\n'
  # write out max column and row weights
  myfile.write(tempstring)
  # write out all of the column weights
  myfile.write(tempstring3)
  # write out all of the row weights
  myfile.write(tempstring1)
  # write out the nonzero indices for each column
  myfile.write(tempstring4)
  # write out the nonzero indices for each row
  myfile.write(tempstring2)
  # close the file
  myfile.close()


class LDPC_matrix:
  """ Class for a LDPC parity check matrix """
  def __init__(self, alist_filename = None,
             n_p_q = None,
             H_matrix = None):
    if (alist_filename != None):
      self.H = self.read_alist_file(alist_filename)
    elif (n_p_q != None):
      self.H = self.regular_LDPC_code_contructor(n_p_q)
    elif (H_matrix != None):
      self.H = H_matrix
    else:
      print 'Error: provide either an alist filename,',
      print 'parameters for constructing regular LDPC parity',
      print 'check matrix, or a numpy array.'

    self.rank = linalg.matrix_rank(self.H)
    self.numRows = self.H.shape[0]
    self.n = self.H.shape[1]
    self.k = self.n -self.numRows

  def regular_LDPC_code_contructor(self,n_p_q):
    """
    This function constructs a LDPC parity check matrix
    H. The algorithm follows Gallager's approach where we create
    p submatrices and stack them together. Reference: Turbo
    Coding for Satellite and Wireless Communications, section
    9,3.

    Note: the matrices computed from this algorithm will never
    have full rank. (Reference Gallager's Dissertation.) They
    will have rank = (number of rows - p + 1). To convert it
    to full rank, use the function get_full_rank_H_matrix
    """

    n = n_p_q[0]  # codeword length
    p = n_p_q[1]  # column weight
    q = n_p_q[2]  # row weight
    # TODO: There should probably be other guidelines for n/p/q,
    # but I have not found any specifics in the literature....

    # For this algorithm, n/p must be an integer, because the
    # number of rows in each submatrix must be a whole number.
    ratioTest = (n*1.0)/q
    if ratioTest%1 != 0:
      print '\nError in regular_LDPC_code_contructor: The'
      print 'ratio of inputs n/q must be a whole number.\n'
      return

    # First submatrix first:
    m = (n*p)/q  # number of rows in H matrix
    submatrix1 = zeros((m/p,n))
    for row in arange(m/p):
      range1 = row*q
      range2 = (row+1)*q
      submatrix1[row,range1:range2] = 1
    H = submatrix1

    # Create the other submatrices and vertically stack them on.
    submatrixNum = 2
    newColumnOrder = arange(n)
    while submatrixNum <= p:
      submatrix = zeros((m/p,n))
      shuffle(newColumnOrder)

      for columnNum in arange(n):
        submatrix[:,columnNum] = \
                      submatrix1[:,newColumnOrder[columnNum]]

      H = vstack((H,submatrix))
      submatrixNum = submatrixNum + 1

    # Double check the row weight and column weights.
    size = H.shape
    rows = size[0]
    cols = size[1]

    # Check the row weights.
    for rowNum in arange(rows):
      nonzeros = array(H[rowNum,:].nonzero())
      if nonzeros.shape[1] != q:
        print 'Row', rowNum, 'has incorrect weight!'
        return

    # Check the column weights
    for columnNum in arange(cols):
      nonzeros = array(H[:,columnNum].nonzero())
      if nonzeros.shape[1] != p:
        print 'Row', columnNum, 'has incorrect weight!'
        return

    return H

def greedy_upper_triangulation(H, verbose=0):
  """
  This function performs row/column permutations to bring
  H into approximate upper triangular form via greedy
  upper triangulation method outlined in Modern Coding
  Theory Appendix 1, Section A.2
  """
  H_t  = H.copy()

  # Per email from Dr. Urbanke, author of this textbook, this
  # algorithm requires H to be full rank
  if linalg.matrix_rank(H_t) != H_t.shape[0]:
      print 'Rank of H:', linalg.matrix_rank(tempArray)
      print 'H has', H_t.shape[0], 'rows'
      print 'Error: H must be full rank.'
      return

  size = H_t.shape
  n = size[1]
  k = n - size[0]
  g = t = 0

  while t != (n-k-g):
    H_residual = H_t[t:n-k-g,t:n]
    size       = H_residual.shape
    numRows    = size[0]
    numCols    = size[1]

    minResidualDegrees = zeros((1,numCols))

    for colNum in arange(numCols):
      nonZeroElements = array(H_residual[:,colNum].nonzero())
      minResidualDegrees[0,colNum] = nonZeroElements.shape[1]

    # Find the minimum nonzero residual degree
    nonZeroElementIndices = minResidualDegrees.nonzero()
    nonZeroElements = minResidualDegrees[nonZeroElementIndices[0],
                                         nonZeroElementIndices[1]]
    minimumResidualDegree = nonZeroElements.min()

    # Get indices of all of the columns in H_t that have degree
    # equal to the min positive residual degree, then pick a
    # random column c.
    indices = (minResidualDegrees == minimumResidualDegree)\
                                     .nonzero()[1]
    indices = indices + t
    if indices.shape[0] == 1:
      columnC = indices[0]
    else:
      randomIndex = randint(0,indices.shape[0],(1,1))[0][0]
      columnC = indices[randomIndex]

    Htemp = H_t.copy()

    if minimumResidualDegree == 1:
      # This is the 'extend' case
      rowThatContainsNonZero = H_residual[:,columnC-t].nonzero()[0][0]

      # Swap column c with column t. (Book says t+1 but we
      # index from 0, not 1.)
      Htemp[:,columnC] = H_t[:,t]
      Htemp[:,t] = H_t[:,columnC]
      H_t = Htemp.copy()
      Htemp = H_t.copy()
      # Swap row r with row t. (Book says t+1 but we index from
      # 0, not 1.)
      Htemp[rowThatContainsNonZero + t,:] = H_t[t,:]
      Htemp[t,:] = H_t[rowThatContainsNonZero + t,:]
      H_t = Htemp.copy()
      Htemp = H_t.copy()
    else:
      # This is the 'choose' case.
      rowsThatContainNonZeros = H_residual[:,columnC-t]\
                                          .nonzero()[0]

      # Swap column c with column t. (Book says t+1 but we
      # index from 0, not 1.)
      Htemp[:,columnC] = H_t[:,t]
      Htemp[:,t] = H_t[:,columnC]
      H_t = Htemp.copy()
      Htemp = H_t.copy()

      # Swap row r1 with row t
      r1 = rowsThatContainNonZeros[0]
      Htemp[r1+t,:] = H_t[t,:]
      Htemp[t,:] = H_t[r1+t,:]
      numRowsLeft = rowsThatContainNonZeros.shape[0]-1
      H_t = Htemp.copy()
      Htemp = H_t.copy()

      # Move the other rows that contain nonZero entries to the
      # bottom of the matrix. We can't just swap them,
      # otherwise we will be pulling up rows that we pushed
      # down before. So, use a rotation method.
      for index in arange (1,numRowsLeft+1):
        rowInH_residual = rowsThatContainNonZeros[index]
        rowInH_t = rowInH_residual + t - index +1
        m = n-k
        # Move the row with the nonzero element to the
        # bottom; don't update H_t.
        Htemp[m-1,:] = H_t[rowInH_t,:]
        # Now rotate the bottom rows up.
        sub_index = 1
        while sub_index < (m - rowInH_t):
          Htemp[m-sub_index-1,:] = H_t[m-sub_index,:]
          sub_index = sub_index+1
        H_t = Htemp.copy()
        Htemp = H_t.copy()

      # Save temp H as new H_t.
      H_t = Htemp.copy()
      Htemp = H_t.copy()
      g = g + (minimumResidualDegree - 1)

    t = t + 1

  if g == 0:
    if verbose:
      print 'Error: gap is 0.'
    return

  # We need to ensure phi is nonsingular.
  T = H_t[0:t, 0:t]
  E = H_t[t:t+g,0:t]
  A = H_t[0:t,t:t+g]
  C = H_t[t:t+g,t:t+g]
  D = H_t[t:t+g,t+g:n]

  invTmod2array = inv_mod2(T)
  temp1  = dot(E,invTmod2array) % 2
  temp2  = dot(temp1,A) % 2
  phi    = (C - temp2) % 2
  if phi.any():
    try:
      # Try to take the inverse of phi.
      invPhi = inv_mod2(phi)
    except linalg.linalg.LinAlgError:
      # Phi is singular
      if verbose > 1:
        print 'Initial phi is singular'
    else:
      # Phi is nonsingular, so we need to use this version of H.
      if verbose > 1:
        print 'Initial phi is nonsingular'
      return [H_t, g, t]
  else:
    if verbose:
      print 'Initial phi is all zeros:\n', phi

  # If the C and D submatrices are all zeros, there is no point in
  # shuffling them around in an attempt to find a good phi.
  if not (C.any() or D.any()):
    if verbose:
      print 'C and D are all zeros. There is no hope in',
      print 'finding a nonsingular phi matrix. '
    return

  # We can't look at every row/column permutation possibility
  # because there would be (n-t)! column shuffles and g! row
  # shuffles. g has gotten up to 12 in tests, so 12! would still
  # take quite some time. Instead, we will just pick an arbitrary
  # number of max iterations to perform, then break.
  maxIterations = 300
  iterationCount = 0
  columnsToShuffle = arange(t,n)
  rowsToShuffle = arange(t,t+g)

  while iterationCount < maxIterations:
    if verbose > 1:
      print 'iterationCount:', iterationCount
    tempH = H_t.copy()

    shuffle(columnsToShuffle)
    shuffle(rowsToShuffle)
    index = 0
    for newDestinationColumnNumber in arange(t,n):
      oldColumnNumber = columnsToShuffle[index]
      tempH[:,newDestinationColumnNumber] = \
                                         H_t[:,oldColumnNumber]
      index +=1

    tempH2 = tempH.copy()
    index = 0
    for newDesinationRowNumber in arange(t,t+g):
      oldRowNumber = rowsToShuffle[index]
      tempH[newDesinationRowNumber,:] = tempH2[oldRowNumber,:]
      index +=1

    # Now test this new H matrix.
    H_t = tempH.copy()
    T = H_t[0:t, 0:t]
    E = H_t[t:t+g,0:t]
    A = H_t[0:t,t:t+g]
    C = H_t[t:t+g,t:t+g]
    invTmod2array = inv_mod2(T)
    temp1  = dot(E,invTmod2array) % 2
    temp2  = dot(temp1,A) % 2
    phi    = (C - temp2) % 2
    if phi.any():
      try:
        # Try to take the inverse of phi.
        invPhi = inv_mod2(phi)
      except linalg.linalg.LinAlgError:
        # Phi is singular
        if verbose > 1:
          print 'Phi is still singular'
      else:
        # Phi is nonsingular, so we're done.
        if verbose:
          print 'Found a nonsingular phi on',
          print 'iterationCount = ', iterationCount
        return [H_t, g, t]
    else:
      if verbose > 1:
        print 'phi is all zeros'

    iterationCount +=1

  # If we've reached this point, then we haven't found a
  # version of H that has a nonsingular phi.
  if verbose:
    print '--- Error: nonsingular phi matrix not found.'

def inv_mod2(squareMatrix):
  """
  Calculates the mod 2 inverse of a matrix.
  """
  A = squareMatrix.copy()
  t = A.shape[0]

  # Special case for one element array [1]
  if A.size == 1 and A[0] == 1:
    return array([1])

  Ainverse = inv(A)
  B = det(A)*Ainverse
  C = B % 2

  # Encountered lots of rounding errors with this function.
  # Previously tried floor, C.astype(int), and casting with (int)
  # and none of that works correctly, so doing it the tedious way.

  test = dot(A,C) % 2
  tempTest = zeros_like(test)
  for colNum in arange(test.shape[1]):
    for rowNum in arange(test.shape[0]):
      value = test[rowNum,colNum]
      if (abs(1-value)) < 0.01:
        # this is a 1
        tempTest[rowNum,colNum] = 1
      elif (abs(2-value)) < 0.01:
        # there shouldn't be any 2s after B % 2, but I'm
        # seeing them!
        tempTest[rowNum,colNum] = 0
      elif (abs(0-value)) < 0.01:
        # this is a 0
        tempTest[rowNum,colNum] = 0
      else:
        if verbose > 1:
          print 'In inv_mod2. Rounding error on this',
          print 'value? Mod 2 has already been done.',
          print 'value:', value

  test = tempTest.copy()

  if (test - eye(t,t) % 2).any():
    if verbose:
      print 'Error in inv_mod2: did not find inverse.'
    # TODO is this the most appropriate error to raise?
    raise linalg.linalg.LinAlgError
  else:
    return C

def swap_columns(a,b,arrayIn):
  """
  Swaps two columns in a matrix.
  """
  arrayOut = arrayIn.copy()
  arrayOut[:,a] = arrayIn[:,b]
  arrayOut[:,b] = arrayIn[:,a]
  return arrayOut

def move_row_to_bottom(i,arrayIn):
  """"
  Moves a specified row (just one) to the bottom of the matrix,
  then rotates the rows at the bottom up.

  For example, if we had a matrix with 5 rows, and we wanted to
  push row 2 to the bottom, then the resulting row order would be:
  1,3,4,5,2
  """
  arrayOut = arrayIn.copy()
  numRows = arrayOut.shape[0]
  # Push the specified row to the bottom.
  arrayOut[numRows-1] = arrayIn[i,:]
  # Now rotate the bottom rows up.
  index = 2
  while (numRows-index) >= i:
    arrayOut[numRows-index,:] = arrayIn[numRows-index+1]
    index = index + 1
  return arrayOut

def get_full_rank_H_matrix(H, verbose=False):
  """
  This function accepts a parity check matrix H and, if it is not
  already full rank, will determine which rows are dependent and
  remove them. The updated matrix will be returned.
  """
  tempArray = H.copy()
  if linalg.matrix_rank(tempArray) == tempArray.shape[0]:
    if verbose:
      print 'Returning H; it is already full rank.'
    return tempArray

  numRows = tempArray.shape[0]
  numColumns = tempArray.shape[1]
  limit = numRows
  rank = 0
  i = 0

  # Create an array to save the column permutations.
  columnOrder = arange(numColumns).reshape(1,numColumns)

  # Create an array to save the row permutations. We just need
  # this to know which dependent rows to delete.
  rowOrder = arange(numRows).reshape(numRows,1)

  while i < limit:
    if verbose:
      print 'In get_full_rank_H_matrix; i:', i
    # Flag indicating that the row contains a non-zero entry
    found  = False
    for j in arange(i, numColumns):
      if tempArray[i, j] == 1:
        # Encountered a non-zero entry at (i, j)
        found =  True
        # Increment rank by 1
        rank = rank + 1
        # Make the entry at (i,i) be 1
        tempArray = swap_columns(j,i,tempArray)
        # Keep track of the column swapping
        columnOrder = swap_columns(j,i,columnOrder)
        break
    if found == True:
      for k in arange(0,numRows):
        if k == i: continue
        # Checking for 1's
        if tempArray[k, i] == 1:
          # Add row i to row k
          tempArray[k,:] = tempArray[k,:] + tempArray[i,:]
          # Addition is mod2
          tempArray = tempArray.copy() % 2
          # All the entries above & below (i, i) are now 0
      i = i + 1
    if found == False:
      # Push the row of 0s to the bottom, and move the bottom
      # rows up (sort of a rotation thing).
      tempArray = move_row_to_bottom(i,tempArray)
      # Decrease limit since we just found a row of 0s
      limit -= 1
      # Keep track of row swapping
      rowOrder = move_row_to_bottom(i,rowOrder)

  # Don't need the dependent rows
  finalRowOrder = rowOrder[0:i]

  # Reorder H, per the permutations taken above .
  # First, put rows in order, omitting the dependent rows.
  newNumberOfRowsForH = finalRowOrder.shape[0]
  newH = zeros((newNumberOfRowsForH, numColumns))
  for index in arange(newNumberOfRowsForH):
    newH[index,:] = H[finalRowOrder[index],:]

  # Next, put the columns in order.
  tempHarray = newH.copy()
  for index in arange(numColumns):
    newH[:,index] = tempHarray[:,columnOrder[0,index]]

  if verbose:
    print 'original H.shape:', H.shape
    print 'newH.shape:', newH.shape

  return newH

def get_best_matrix(H, numIterations=100, verbose=False):
  """
  This function will run the Greedy Upper Triangulation algorithm
  for numIterations times, looking for the lowest possible gap.
  The submatrices returned are those needed for real-time encoding.
  """

  hadFirstJoy = 0
  index = 1
  while index <= numIterations:
    if verbose:
      print '--- In get_best_matrix, iteration:', index
    index += 1
    try:
      ret = greedy_upper_triangulation(H, verbose)
    except ValueError, e:
      if verbose > 1:
        print 'greedy_upper_triangulation error: ', e
    else:
      if ret:
        [betterH, gap, t]
      else:
        continue

      if not hadFirstJoy:
        hadFirstJoy = 1
        bestGap = gap
        bestH = betterH.copy()
        bestT = t
      elif gap < bestGap:
        bestGap = gap
        bestH = betterH.copy()
        bestT = t


  if hadFirstJoy:
    return [bestH, bestGap]
  else:
    if verbose:
      print 'Error: Could not find appropriate H form',
      print 'for encoding.'
    return

def getSystematicGmatrix(H):
  """
  This function finds the systematic form of the generator
  matrix G. The form is G = [I P] where I is an identity matrix
  and P is the parity submatrix. If the H matrix provided
  is not full rank, then dependent rows will be deleted.
  """
  tempArray = H.copy()
  numRows = tempArray.shape[0]
  numColumns = tempArray.shape[1]
  limit = numRows
  rank = 0
  i = 0
  while i < limit:
    # Flag indicating that the row contains a non-zero entry
    found = False
    for j in arange(i, numColumns):
      if tempArray[i, j] == 1:
        # Encountered a non-zero entry at (i, j)
        found = True
        # Increment rank by 1
        rank = rank + 1
        # make the entry at (i,i) be 1
        tempArray = swap_columns(j,i,tempArray)
        break
    if found == True:
      for k in arange(0,numRows):
        if k == i: continue
        # Checking for 1's
        if tempArray[k, i] == 1:
          # add row i to row k
          tempArray[k,:] = tempArray[k,:] + tempArray[i,:]
          # Addition is mod2
          tempArray = tempArray.copy() % 2
          # All the entries above & below (i, i) are now 0
      i = i + 1
    if found == False:
      # push the row of 0s to the bottom, and move the bottom
      # rows up (sort of a rotation thing)
      tempArray = moveRowToBottom(i,tempArray)
      # decrease limit since we just found a row of 0s
      limit -= 1
  # the rows below i are the dependent rows, which we discard
  G = tempArray[0:i,:]
  return G
