#! /usr/local/bin/tclsh8.0 
# text_serv
# 	This file implements a server for a text editor and handles 
#	file operations


global sockFileInfo	# information about  client
global homepath
global updateNotSent
global updateAllClients
global DEBUG
global ERROR
set ERROR(permission) -2 
set ERROR(clash) -3 
set DEBUG 1 		
# Debug mode 
set DIR_NOT_FOUND 	1 	
# Directory not found  errno
# Number of command line arguments
set PORT_NUMBER 	2542
set homepath [pwd]
# ldelete
#
#	procedure to delete a list element by value
#	Arguments:	
#		list : The list from which element needs to be deleted
#		value: Value of element to be deleted
#
#	Returns:
#		a new list 

proc ldelete { list value } {
	set index [lsearch -exact $list $value]
	if { $index >= 0 }  {
		return [lreplace $list $index $index]
	} else {
		return $list
	}
}

# lfindallindices
#
#	Find all instances of value in list
#
#	Search list for first and last occurance of the element
# 	Element occurs contiguously only
#       Arguments:
#               lists: Temporary variable to hold List
#               value: pidValue of client 
proc lfindallindices { lists value } {
        set firstindex [lsearch -exact $lists $value]
	# set the first owned line as firstindex
        if { $firstindex >=0 } {
                set endindex  $firstindex
		# set the end of owned region initially as firstindex
                set index $firstindex
        	while { $index >= 0 } {
               		set lists [lreplace $lists $index $index]
			# remove already found element 
                	set index [lsearch -exact $lists $value]
			# search for next element 
                	if { $index >= 0 } {
                       	 incr endindex
			# if next element found increment endindex
                	}
        	}
        	set indices [list $firstindex $endindex]
		return $indices
	} else {
		return $firstindex
		# return NOTFOUND
	}
}

# delFileName  
# 
# 	delete the fileName from list of open files, unless file is open for
#	another user
#	Arguments:
#		fileName: the file to be deleted
#		sock: The socket whose entry needs to be deleted

proc delFileName { sock }  {
	global sockFileInfo	# information about  client
	global fileShareInfo
	global lines
	global extent
	global owners
	global path
	global homepath
	global pidVal
	if { ![info exists path(new,$sock)] } {
		set path(orig,$sock) $homepath 
		set path(new,$sock) $homepath
#		puts "PWD is $path(new,$sock) for $sock"	
	}
	set fileName  $sockFileInfo(fileName,$sock)
	incr fileShareInfo(shared,$fileName) -1 	
#	puts "File Shared" 
#	puts $fileShareInfo(shared,$fileName)
	unset sockFileInfo(fileName,$sock)
	# delete sharedinfo
	catch {unset extent($pidVal($sock),$fileName)}
	if { $fileShareInfo(shared,$fileName) == 0} {
		unset fileShareInfo(shared,$fileName)
		unset fileShareInfo(sock,$fileName)
		if { [info exists owners($fileName)] } {
			unset owners($fileName)
			# unset each extent  then the owner list 
		}
		unset lines(content,$fileName)
	} else {
		set fileShareInfo(sock,$fileName) [ldelete $fileShareInfo(sock,$fileName) $sock] 
		append val ^s
		append val $pidVal($sock)
		append val ^s
		set lines(content,$fileName) [ldelete $lines(content,$fileName) $val]
		# delete tag from text 
		unset val
		# delete owned region information
		set owners($fileName) [ldelete $owners($fileName) $pidVal($sock)] 
		updateServerBufferAll $fileName "ALL" $sock "NO"
	}	
}

# textServer --
#
#	Open a server socket which calls acceptConnections to handle
#       clients requests
# 	Arguments:
#		port 	The local port at which the server listens
#			for clients requests

proc textServer {port} {
	global serverPort 
	global DEBUG
	set serverPort [socket -server acceptConnections $port]
	if $DEBUG {
		puts "ServerPort is $serverPort"
	}

}

# acceptConnections
# 	Set up line buffering and register procedure for event handling
#	Also set the global variable fileType to *
# 	Arguments:
#		sock The name of new channel created for connected client
#		addr The address in network address notation of the client
#		port The client's port number

proc acceptConnections {sock addr port} {
	global fileType	

#	puts "Accept $sock from $addr port $port"
	fconfigure $sock -buffering line 	
	# Allow line buffering 
	fileevent $sock readable [list museServer  $sock]
	# invoke  muse server whenever socket is readable
	set fileType($sock)  *
}

# museServer
#	Interpret  data from channel and take appropriate action
# 	
#	Arguments:
#		sock Channel for connected client 

proc museServer {sock} {
	global sockFileInfo	# information about  client
	global serverPort
	global homepath
	global path
	global DEBUG
	if {[eof $sock] || [catch {gets $sock line } ]} {
		# end of file or abnormal connection drop
		if {[info exists sockFileInfo(fileName,$sock)]} {
			delFileName $sock
		}	
		# delete the file open for this socket
		close $sock
		if $DEBUG {
			puts "Close socket $sock"
		}
	} else {
#		puts "$line"
		switch -regexp -- $line { 
			{quit}	{ # Prevent new connections 
				  # Existing connections stay open

				# puts "Close Client"
				#close $serverPort 
			}  
			{setPWD} { #Get directory listing 
				set path(orig,$sock) $homepath 
				set path(new,$sock) $homepath
				if $DEBUG {
					puts "PWD is $path(new,$sock) for $sock"
				}
			}
			{getDir} { #Get directory listing
				if { ![info exists path(new,$sock)] } {
					set path(orig,$sock) $homepath 
					set path(new,$sock) $homepath
	#				puts "PWD is $path(new,$sock) for $sock"	
				}
			#	puts "$sock"
				sendDirs $sock	
			}
			{chDir} { 
				# change to Directory
				regexp {chDir,(.*)} $line cdreq cdDir
				set newpath  [file join $path(new,$sock) $cdDir]
				if [catch {cd $newpath}] {
				#	puts "Directory not found\n"	 
					sendErrno $sock $DIR_NOT_FOUND
				} else {
					set path(new,$sock) [pwd]
					cd $homepath
				}
			}
			{restoreDirectory} { #restore the pwd
				set path(new,$sock) $path(orig,$sock)
			}
			{chFileType} { # change File Type
				global fileType
				regexp {chFileType,(.*)} $line cfReq fileType($sock)
				# fileType is a global variable with one value
				# for each client
				# send the new file list 
				sendFileList $sock 
			}
			{clientOpen} {
			 # serverPopDocument
				# Client requests document for 
			     	# the first time.
			     	# send the whole file and add to shared
				global sockFileInfo
				global fileShareInfo				
				global pidVal
				set path(orig,$sock) $path(new,$sock)			
				# client sends this request when in final 
				# directory 
				regexp {clientOpen,([^,]+),(.*)} $line textReq fileName pidvl
				# send the file 
				# store file name for client
				set pidVal($sock) $pidvl
				# pidVal stored for Client
				set fileName [file join $path(new,$sock) $fileName]
				set sockFileInfo(fileName,$sock) $fileName
				if {[info exists fileShareInfo(shared,$fileName)]} {
				# if file is already open 
					incr fileShareInfo(shared,$fileName) 
					lappend fileShareInfo(sock,$fileName) $sock 
					if $DEBUG {
						puts "File Shared" 
						puts $fileShareInfo(shared,$fileName)
					}
				} else {
				 # else open file 
					set fileShareInfo(shared,$fileName) 1
					set fileShareInfo(sock,$fileName) $sock
					if $DEBUG {
						puts "File Shared"
						puts $fileShareInfo(shared,$fileName)
					}
				}
				updateServerBufferAll $fileName $sock $sock "YES"
				#Lazy sharing, update server from all clients
			}
			{sendFileList} { # send the whole file list
				sendFileList $sock 
			}
			{saveFile} { # save all changes to file 
				regexp {saveFile,([^,]+),(.*)} $line saveReq fileName saveText
				set path(orig,$sock) $path(new,$sock)
				set fileName [file join $path(new,$sock) $fileName]
				saveFile $sock $saveText $fileName
			}
			{saveasFile} { # change file to be saved in 
				regexp {([^,]+),([^,]+),(.*)} $line saveReq saveas fileName saveText
				set path(orig,$sock) $path(new,$sock)
				set fileName [file join $path(new,$sock) $fileName]
				saveasFile $saveText $fileName 
				
			}
			{creatFile} { # create a new file 
				global fileShareInfo
				global pidVal
				global extent
				regexp {([^,]+),([^,]+),(.*)} $line saveReq creatFile fileName pidvl 
				set path(orig,$sock) $path(new,$sock)
				set fileName [file join $path(new,$sock) $fileName]
				creatFile $sock $fileName 		

			}
			{newFile} { # Clear previous file
				regexp {newFile,(.*)} $line newReq fileName
				delFileName $sock		
						
			}
			{updateOwnedList} { # Update the ownership list
				#### new stuff	
				global pidVal
				regexp {updateOwnedList,([^,]+),([^,]+),([^,]+),(.*)} $line updtReq fileName  adjacentPidVal offsetFromAdjacent ownedLines
				# puts $line
				# puts "FileName is "
				# puts $fileName
				set fileName [file join $path(new,$sock) $fileName]
				updateOwnedList $fileName $pidVal($sock) $adjacentPidVal $offsetFromAdjacent $ownedLines
				updateServerBufferAll $fileName "ALL" $sock "YES"
			}
			{serverPushDocument} { 
				# Update the buffer to 
				# reflect the latest changes
				global updateNotSent
				global pidVal
		#		puts $pidVal($sock)
				# puts $line
			 	regexp  {serverPushDocument,([^,]+),(.*)} $line updtReq fileName newContents 
				# puts $fileName
				set fileName [file join $path(new,$sock) $fileName]
				# puts $line 
				if { $newContents != " " } {
					updateBuffer $fileName $pidVal($sock) $newContents 
					# Update the buffer for file
					unset newContents
				}
				if {[info exists updateNotSent($fileName)]} {
					incr updateNotSent($fileName) -1
					if { $updateNotSent($fileName) ==0 } {
						unset updateNotSent($fileName)
						updateClients $fileName	
					}	
				}
			}
			{updateServerAllBuffer} { #Get latest buffer
					       #  from all the clients
				global fileShareInfo
				global updateNotSent
				global path
			 	regexp  {updateServerAllBuffer,([^,]+),(.*)} $line updtReq  fileName updateAll
				# puts $fileName
				set fileName [file join $path(new,$sock) $fileName]
				if { $updateAll == "ONE" } { 
					set updateAll $sock
				}
				# puts $updateAll
				updateServerBufferAll $fileName $updateAll "YES" "No"
			}
		}
	}
}

# updateServerBufferAll
# 
#	Arguments:
#		fileName: Name of file whose buffer needs to be updated
#		updateAll: Whether all clients need to be updated or 
#			   just the  requesting client 
#		clientSock: send to which client
#		doNotRequest: do not have anything to update server with
#		

proc updateServerBufferAll   { fileName updateAll clientSock doNotRequest } {
	global fileShareInfo
	global updateNotSent
	global updateAllClients

        set count 0
        set flag 1
        set updateAllClients($fileName)  $updateAll
	# Update one client/all clients
        unset updateAll
	# append text_out requestForUpdate,
	append text_out serverPushDocumentRequest,
	# Get updates from all clients 
	if  { $doNotRequest == "YES" } {
		# client does not have any data to update client with
		set socketList [ldelete $fileShareInfo(sock,$fileName) $clientSock]
		# delete client from clients to get update from 
	} else {	
		set socketList $fileShareInfo(sock,$fileName)
	}
        foreach sock $socketList {
               		puts $sock $text_out
			flush $sock
               		incr count 
        } 
	if { $count !=0 } {
        	set updateNotSent($fileName) $count 
		# global variable updateNotSent
	} else {
		updateClients $fileName
		# only one client requesting for update 
	}
	unset text_out
}

# updateClients 
# 
#	Arguments:
#		fileName: Name of file whose buffer needs to be updated
#		

proc updateClients { fileName } {
	global updateAllClients
	global fileShareInfo
	if { $updateAllClients($fileName) =="ALL" } {	
        	foreach sock $fileShareInfo(sock,$fileName) {
               		sendDocumentToClient $sock $fileName	
        	}
	} else {
               	sendDocumentToClient $updateAllClients($fileName) $fileName 
	}
}
# updateBuffer
# 
#	Arguments:
#		fileName: Name of file whose buffer needs to be updated
#		newContents: The new contents of the file
#		pidVal: Identifier for client whose 
#		contents need to be updated

proc updateBuffer   { fileName pidvl newContents } {
	global lines 
	global extent
 	append val ^s	
	append val $pidvl
 	append val ^s	
	# puts "Updating Buffer"
	set index [ lsearch -exact $lines(content,$fileName) $val ]
	if {[info exists extent($pidvl,$fileName)]} {
		# puts "updateBuffer:extent is "
		set lastindex  $extent($pidvl,$fileName)
		# puts $lastindex
		set lines(content,$fileName) [lreplace $lines(content,$fileName) [expr $index+1] [expr $index+$lastindex ]]
	 }
	# delete previously owned lines
	set numLines 0
   	foreach element $newContents {	
		# puts $element
		set lines(content,$fileName) [linsert $lines(content,$fileName) [expr $index+1]  $element]
		incr index
		incr numLines   
		# increment number of lines by 1
	}
	set extent($pidvl,$fileName) $numLines
	# set extent to new value
	# Update to new buffer 
}
	
# updateOwnedList
#
#	Arguments:
#		pidvl 		PID value of the client 
#		newOwned 	The new area to be owned
#		numOfLines 	The new number of lines owned
#		position 	The previous owner's last line
proc updateOwnedList { fileName pidvl adjacentPidVal offsetFromAdjacent ownedLines } {
	global extent
	global lines
	global owners
	# extent is an array of number of lines owned by each of the owner
	# puts " Updating owned list extent"
#	if { exists owners($fileName)] || [lsearch -exact $owners($fileName) $pidvl ] == -1} 
	if {$extent($pidvl,$fileName)==0} {
	# pidval does not exist in extent list 
		lappend owners($fileName) $pidvl 
		# puts $owners($fileName)
		# keep a list of owners also
	}
	set extent($pidvl,$fileName) $ownedLines
	# update array to have new number of lines owned 

	append val ^s
	append val $pidvl
	append val ^s
	# puts "server has the following lines "
	# puts $lines(content,$fileName)
	set lines(content,$fileName) [ldelete $lines(content,$fileName) $val]
	unset val
	# delete tag from previously  owned index
	insertOwner  $fileName $pidvl  $adjacentPidVal  $offsetFromAdjacent
	# insert new owner for fileName by inserting tag in the buffer
	# if adjacentpidVal  is TOP, then insert tag from the top
	# update all clients that list has changed by sending 
	# buffer and extent   
}

proc insertOwner { fileName pidvl adjacentPidVal position } {
	global extent
	global lines
	global ERROR
	
	if { $adjacentPidVal == "TOP" } { 
		set index $position
	} else {
		append val ^s
		append val $adjacentPidVal
		append val ^s
		# adjacent owner tag in the buffer
		set adjacentIndex [ lsearch -exact $lines(content,$fileName) $val ]
		# find index of adjacent tag 
		# puts "insertOwner: extent is "
		# puts $extent($adjacentPidVal,$fileName)
		set index [expr $adjacentIndex + $extent($adjacentPidVal,$fileName)+$position]
		# The new tag is to be inserted  underneath the previous tag 
		# at new position.
		set lastIndex [expr $index + $extent($pidvl,$fileName)]
	 	# puts $lastIndex
		set startPlace [expr $adjacentIndex + $extent($adjacentPidVal,$fileName)]
#		puts $startPlace
		set templist [lrange $lines(content,$fileName) $startPlace $lastIndex]
		set nextVal 0
		foreach value $templist {
			incr nextVal
			set foundindex [string first $value ^s]
			if { $foundindex != -1 } {
				set foundlast [string last $value ^s]
				set foundlast [expr $foundlast -1]
				set foundindex [expr $foundindex +1]
				set id [string range $value $foundindex $foundlast]
				break
			}
				
		} 
		set finalVal [expr $startPlace + $nextVal] 
	#	puts $finalVal
		if {$finalVal > $index && $finalVal < $lastIndex} {
			set CLASH 1
			puts "CLASH"
			# trying to lock the same region 
 			append text_out ERROR,
			append text_out $ERROR(clash)
		} elseif { $foundindex != -1 } { 
			#find clashing region 
			set lastIndex [expr $finalVal + $extent($id,$fileName)]
	 #		puts $lastIndex
			if { $finalVal > $index }  {
				set CLASH 1 
				puts "CLASH"
 			append text_out ERROR,
			append text_out $ERROR(clash)
			}	
		} 
		# find any  tags in region to be locked 
		# find pidVal
		# if foundindex is less than index, then find pidval 
		# and find extent 
		unset val
	}
	append val \^s $pidvl \^s
	set lines(content,$fileName) [linsert  $lines(content,$fileName) $index $val]
	# insert new tag  
}

	
# saveFile
# 
# 	
#   	Arguments: 
#	        saveText	Text to be sent to the client 
# 		fileName 	The file requested by the client		
proc saveFile {sock saveText fileName } {
	if [file writable $fileName] {
 	 	regsub -all {\^A} $saveText "\n" saveText 
		set filePatchHandle [open patchapply.$sock w]
 		puts -nonewline $filePatchHandle $saveText
		close $filePatchHandle
		if [catch {exec patch <patchapply.$sock $fileName} errorcode] {
		} else {
		 	# puts "Could not save file"	
		}
		file delete -force patchapply.$sock
		unset saveText
		unset errorcode
	}
}


# saveasFile
# 
#	Read in the whole file into buffer. Replace \n by ^A. Send whole
#	file as one line to client.
# 	
#   	Arguments: 
# 		fileName 	The file requested by the client		

proc saveasFile {saveText fileName } {
	set file_handle [open $fileName w] 
 	regsub -all {\^A} $saveText "\n" saveText 
 	puts -nonewline $file_handle $saveText
	close $file_handle
	unset saveText
}

proc creatFile {sock fileName} {
	sendDocumentToClient $sock $fileName

}
# sendDocumentToClient
# 
#	Read in the whole file into buffer. Replace \n by ^A. Send whole
#	file as one line to client.
# 	
#   	Arguments: 
#		sock 		Channel for connected client
#		fileName 	Name of file 
proc sendDocumentToClient {sock fileName } {
	global sockFileInfo	# information about  client
	global fileShareInfo	# information about file
	global lines 		# array containing file contents 	
	global owners		# list of clients currently editing the 
				# document
	global extent 		# The number of lines locked by a client
	global pidVal
	global new
	global extent
	global ERROR
	# puts "sendDocumentToClient" 
	# if this is the first client requesting the file
	# make a list of all lines and set owner as free
	if {![info exists lines(content,$fileName)]} {	
	   if { [file exists $fileName] } {
		# if file exists open it for read/ write  depending on 
		# permissions 
		if [file writable $fileName] {
	#		puts "writable file"
			set file_handle [open $fileName r+]
		} else {
	#		puts "read only file"
			set file_handle [open $fileName r]
		}
		set line_out [read $file_handle] 
		if { $line_out == "" } {
			set line_out " "
		}
		set lines(content,$fileName) [split $line_out \n]
		# keep the open file in a buffer
	   } else { 	
		# create a new file, if successful initialize 
		# global data structures 
		# set file_handle [open $fileName w]
		if [ catch {open $filename w} file_handle ] {
			puts "Permission Denied" 
 			append text_out ERROR,
			append text_out $ERROR(permission)
			puts $sock $text_out
			# send to client
			flush $sock
			unset text_out
			# send the access list and the file to client
			return 
		} else {
			# Create the file in the requested directory 
			close $file_handle
			set sockFileInfo(fileName,$sock) $fileName
			set fileShareInfo(shared,$fileName) 1
			set fileShareInfo(sock,$fileName) $sock
			set pidVal($sock) $pidvl
			set extent($pidvl,$fileName) 0
			set lines(content,$fileName) " "
			updateOwnedList $fileName $pidVal($sock) TOP 0 1
			# user owns first line 
			set ERROR 0
		}
	   }
		
	}
	
 	append text_out textDisplay,
	# append text_out [array get extent]
	
	if {[info exists owners($fileName)]} {
		# puts "There are owners"
        	foreach pid $owners($fileName) {
			lappend temp $pid
			lappend temp $extent($pid,$fileName)
		}
	}
	if { ![info exists temp]} { 
		set temp " " 
	}
	# puts "Owner is "
	# puts $temp
 	append text_out $temp
	# append accessList 
	# This list tells the number of lines owned by each client
 	append text_out ,
	append text_out $lines(content,$fileName)
	# append new file contents
	# this list also has the starting position of owned lines
	# of each client
	puts $sock $text_out
	# send to client
	flush $sock
	unset text_out
	# send the access list and the file to client
	return 
}

# sendDirs
# 
#	Get directory list for current directory. Send to client
# 	
#   	Arguments: 
#		sock 		Channel for connected client

proc sendDirs {sock} {
	global path

#	puts "Path is $path(new,$sock)"
	append dirpath $path(new,$sock)
	append dirpath /*/
	set list_of_directories [glob -nocomplain $dirpath]
	unset dirpath
	set num_of_directories [llength $list_of_directories]
	set no_of_bytes [string length $list_of_directories]
	append dir_out dirList,
	while {$num_of_directories>0} {
		set num_of_directories  [expr $num_of_directories-1]
		set cur_dir [lindex $list_of_directories $num_of_directories]
		append dir_out ^A
		append dir_out [file tail $cur_dir]
	}
	puts $sock  $dir_out
	flush $sock
	unset dir_out
	unset list_of_directories no_of_bytes
	unset num_of_directories
	return
}

# send to client an error response 
#
#	Arguments: errno The error number 
#		   sock	 The channel for sending errno to client 

proc sendErrno {sock, errno} {
	puts $sock  $errno
	flush $sock
	return

}
# sendFileList
# 
#	Get file list for current directory. Send to client
# 	
#   	Arguments: 
#		sock 		Channel for connected client
#		fileType 	The type of file to be searched

proc sendFileList {sock } {
	global fileType
	global path
	append filepath $path(new,$sock)
	append filepath /
	append filepath $fileType($sock)
	set list_of_files [glob -nocomplain $filepath]
	unset filepath
	set num_of_files [llength $list_of_files]
	set no_of_bytes [string length $list_of_files]
	append file_out fileList,
	while {$num_of_files>0} {
		set num_of_files  [expr $num_of_files-1]
		set cur_file [lindex $list_of_files $num_of_files]
		if [file isfile  $cur_file] {
			if [file readable $cur_file] {	
				append file_out ^A
				append file_out [file tail $cur_file ]
			}
		}
	}
	puts $sock $file_out
	flush $sock
	unset file_out
	unset no_of_bytes
	unset num_of_files list_of_files
	return 
}



# Start server and wait forever 
if { $argc == 0  }  {
	set port $PORT_NUMBER	
} else {
	set port [lindex $argv 0]
}
puts "Port is $port"
textServer  $port
vwait forever

