#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
:Brief: Produces rand disjoint communities (clusters) for the given network with sizes similar in the ground truth.
:Description:
	Takes number of the resulting communities and their sizes from the specified groundtruth (actually any sample
	of the community structure, the real ground truth is not required) and fills stubs of the clusters with
	randomly selected nodes from the input network with all their neighbors.
	Note: Produced result is a random disjoint partitioning, so if the 'ground truth' had overlapping clusters, then
	the number of nodes in the last cluster will be less than in the sample.

:Authors: Artem Lutov <luart@ya.ru>
:Organizations: eXascale lab <http://exascale.info/>, ScienceWise <http://sciencewise.info/>,
	Lumais <http://www.lumais.com/>
:Date: 2015-07
"""

from __future__ import print_function, division  # Required for stderr output, must be the first import
import sys
import os  # Pathes processing
#import igraph as ig
import random as rand
try:
	# ATTENTION: Python3 newer treats imports as realtive and results in error here unlike Python2
	from utils.parser_nsl import asymnet, loadNsl  #pylint: disable=E0611,E0401
except ImportError:
	# Note: this case should be the second because explicit relative imports cause various errors
	# under Python2 and Python3, which complicates thier handling
	from .utils.parser_nsl import asymnet, loadNsl  #pylint: disable=E0611,E0401

# Default number of the resulting clusterings (partitions, i.e files that contain disjoint clusters)
_RESNUM = 1


class Params(object):
	"""Input parameters (arguments)"""
	def __init__(self):
		"""Parameters:
		groundtruth  - flile name of the ground truth clustering
		network  - flile name of the input network
		dirnet  - whether the input network is directed
		outnum  - number of the resulting clusterings
		randseed  - seed for the clustering generation (automatically generated if not specified)
		outpseed  - whether to output the seed (automatically set to True on if the seed is generated automatically)
		outdir  - output directory
		outname  - base name of the output file based on the network name
		outext  - extenstion of the output files based on the groundtruth extension
		"""
		self.groundtruth = None
		self.network = None
		self.dirnet = False
		self.outnum = _RESNUM
		self.randseed = None
		self.outpseed = False
		self.outdir = None
		self.outname = None
		self.outext = ''


def parseParams(args):
	"""Parse user-specified parameters

	returns  - parsed input arguments, Params()
	"""
	assert isinstance(args, (tuple, list)) and args, 'Input arguments must be specified'
	prm = Params()

	for arg in args:
		# Validate input format
		preflen = 3
		if arg[0] != '-' or len(arg) <= preflen:
			raise ValueError('Unexpected argument: ' + arg)

		if arg[1] == 'g':
			prm.groundtruth = arg[preflen:]
			prm.outext = os.path.splitext(prm.groundtruth)[1]
		elif arg[1] == 'i':
			pos = arg.find('=', 2)
			if pos == -1 or arg[2] not in 'ud=' or len(arg) == pos + 1:
				raise ValueError('Unexpected argument: ' + arg)
			pos += 1
			prm.network = arg[pos:]
			prm.outname, netext = os.path.splitext(os.path.split(prm.network)[1])
			prm.dirnet = asymnet(netext.lower(), arg[2] == 'd')
			if not prm.outname:
				raise ValueError('Invalid network name (is a directory): ' + prm.network)
		elif arg[1] == 'n':
			prm.outnum = int(arg[preflen:])
			assert prm.outnum >= 1, 'outnum must be a natural number'
		elif arg[1] == 'r':
			prm.randseed = arg[preflen:]
		elif arg[1] == 'o':
			prm.outdir = arg[preflen:]
		else:
			raise ValueError('Unexpected argument: ' + arg)

	if not (prm.groundtruth and prm.network):
		raise ValueError('Input network and groundtruth file names must be specified')
	if not prm.outdir:
		prm.outdir = os.path.split(prm.network)[0]
		if not prm.outdir:
			prm.outdir = '.'
	if not prm.randseed:
		try:
			prm.randseed = ''.join(str(ord(c)) for c in os.urandom(8))
		except NotImplementedError:
			prm.randseed = str(rand.random())
		prm.outpseed = True

	return prm


def randcommuns(*args):
	"""Generate random clusterings for the specified network"""
	prm = parseParams(args)
	print('Starting randcommuns clustering:'
	 '\n\tgroundtruth: {}'
	 '\n\t{} network: {}'
	 '\n\t{} cls of {} in {} with randseed: {}'
	 .format(prm.groundtruth, 'directed' if prm.dirnet else 'undirected', prm.network
	  , prm.outnum, prm.outname + prm.outext, prm.outdir, prm.randseed))
	# Load Data from simple real-world networks
	graph = loadNsl(prm.network, prm.dirnet)  # ig.Graph.Read_Ncol(network, directed=dirnet)  # , weights=False

	# Load statistics from the ground thruth
	groundstat = []
	with open(prm.groundtruth, 'r') as fground:
		for line in fground:
			# Skip empty lines and comments (possible header)
			if not line or line[0] == '#':
				continue
			groundstat.append(len(line.split()))

	# Create outpdir if required
	if prm.outdir and not os.path.exists(prm.outdir):
		os.makedirs(prm.outdir)
	# Geneate rand clsuterings
	rand.seed(prm.randseed)
	while prm.outnum > 0:
		prm.outnum -= 1
		# Active (remained) nodes indices of the input network
		actnodes = set(graph.vs.indices)  #pylint: disable=E1101
		clusters = []  # Forming clusters
		# Reference size of the ground truth clusters (they migh have overlaps unlike the current partitioning)
		for clmarg in groundstat:
			nodes = []  # Content of the current cluster
			# Check whether all nodes of the initial network are mapped
			if not actnodes:
				break
			# Select subsequent rand node
			ind = rand.sample(actnodes, 1)[0]
			actnodes.remove(ind)
			nodes.append(ind)
			inode = 0  # Index of the node in the current cluster
			# Select neighbors of the selected nodes to fill the clusters
			while len(nodes) < clmarg and actnodes:
				for nd in graph.vs[nodes[inode]].neighbors():  #pylint: disable=E1136
					if nd.index not in actnodes:
						continue
					actnodes.remove(nd.index)
					nodes.append(nd.index)
					if len(nodes) >= clmarg or not actnodes:
						break
				inode += 1
				if inode >= len(nodes) and len(nodes) < clmarg and actnodes:
					ind = rand.sample(actnodes, 1)[0]
					actnodes.remove(ind)
					nodes.append(ind)

			# Use original labels of the nodes
			clusters.append(graph.vs[ind]['name'] for ind in nodes)  #pylint: disable=E1136
		# Output resulting clusters
		with open('/'.join((prm.outdir, ''.join((prm.outname, '_', str(prm.outnum), prm.outext)))), 'w') as fout:
			for cl in clusters:
				# Note: print() unlike fout.write() appends the newline
				print(' '.join(cl), file=fout)

	# Output randseed used for the generated clusterings
	# Output to the dir above if possible to not mix cluster levels with rand seed
	if prm.outpseed:
		with open('/'.join((prm.outdir, (os.path.splitext(prm.outname)[0] + '.seed'))), 'w') as fout:
			# Note: print() unlike fout.write() appends the newline
			print(prm.randseed, file=fout)
	print('Random clusterings are successfully generated')


if __name__ == '__main__':
	if len(sys.argv) > 2:
		randcommuns(*sys.argv[1:])
	else:
		print('\n'.join(('Produces random disjoint partitioning (clusters are formed with rand nodes and their neighbors)'
			' for the input network specified in the NSL format (generalizaiton of NCOL, SNAP, etc.)\n',
			'Usage: {app} -g=<ground_truth> -i[{{u, d}}]=<input_network> [-n=<res_num>] [-r=<rand_seed>] [-o=<outp_dir>]',
			'',
			'  -g=<ground_truth>  - ground truth clustering as a template for sizes of the resulting communities',
			'  -i[X]=<input_network>  - file of the input network in the format: <src_id> <dst_id> [<weight>]',
			'    Xu  - undirected input network (<src_id> <dst_id> implies also <dst_id> <src_id>). Default',
			'    Xd  - directed input network (both <src_id> <dst_id> and <dst_id> <src_id> are specified)',
			'    NOTE: (un)directed flag is considered only for the networks with non-NSL file extension',
			'  -n=<res_num>  - number of the resulting clusterings to generate. Default: {resnum}',
			'  -r=<rand_seed>  - random seed, string. Default: value from the system rand source (otherwise current time)',
			'  -o=<output_communities>  - . Default: ./<input_network>/'
		)).format(app=sys.argv[0], resnum=_RESNUM))
