# deployment.py
#
# Copyright (C) 2013 to 2014 Tim Marston <tim@edm.am>
#
# This file is part of stdhome (hereafter referred to as "this program").
# See http://ed.am/dev/stdhome for more information.
#
# This program 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.
#
# This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.


import os, re, shutil, filecmp, json
import the, util
from walker.copy_in import CopyInWalker
from walker.conflict import ConflictWalker
from walker.copy_out import CopyOutWalker


class Deployment:


	def __init__( self ):
		if the.repo is None:
			raise RuntimeError( 'logic error: Deployment initialised when '
								'the.repo is unset' )
		self.load_deployment_state()
		self.conflicts_checked = False


	def load_deployment_state( self ):
		"""Load any deployment state.  If one is found then a deployment will be
		considered to be ongoing.
		"""

		# list of files that were copied-in (or at least given the opportunity
		# to be) and updated through the vcs update.  This means that, while
		# there may have been conflicts during the update (which the user will
		# have to have dealt with in the repo), any conflicts arising with these
		# files in the home directory are no longer important and can be
		# ignored.  In short, this is a list of files that can safely be
		# deployed, regardless of the state of the home directory.
		self.imported_files = None

		# list of files that were affected by a recent vcs update (so only these
		# files need to be checked for deployment conflicts or copied-out)
		self.affected_files = None

		# the revno that the repo was as prior to a recent update
		self.initial_revno = None

		# do we have a repo?
		if not os.path.exists( the.repo.full_dir ): return

		# if no file list exists, we're done
		file = os.path.join( the.full_mddir, "deploy.%s" % the.repo.name )
		if not os.path.isfile( file ):
			return

		# read the file list
		if the.verbose >= 1: print "deployment state found; loading"
		f = open( file, 'r' )
		state = json.loads( f.read() )

		# unpack deployment state
		if 'imported_files' in state:
			self.imported_files = state['imported_files'];
		if 'initial_revno' in state:
			self.initial_revno = state['initial_revno'];
		if 'affected_files' in state:
			self.affected_files = state['affected_files'];


	def save_deployment_state( self ):
		"""Save the current deployment state (so there will be a deployment ongoing).
		"""

		if the.verbose >= 1: print "saving deployment state"

		# create metadata directory, as necessary
		if not os.path.isdir( the.full_mddir ):
			os.mkdir( the.full_mddir )

		# pack deployment state
		state = {
			'imported_files': self.imported_files,
			'initial_revno': self.initial_revno,
			'affected_files': self.affected_files,
		}

		# create file
		file = os.path.join( the.full_mddir, "deploy.%s" % the.repo.name )
		f = open( file, 'w' )
		f.write( json.dumps( state ) );


	def remove_deployment_state( self ):
		"""Remove the current deployment state (so no deployment will be ongoing).
		"""

		# check it exists
		file = os.path.join( the.full_mddir, "deploy.%s" % the.repo.name )
		if( os.path.isfile( file ) ):

			# delete it
			if the.verbose >= 1: print "removing deployment state"
			os.unlink( file )


	def is_ongoing( self ):
		"""Is there a deployment currently ongoing?
		"""

		return False if self.imported_files is None else True


	def check_ongoing( self, ongoing = True ):
		"""Check that a deployment either is or is not ongoing and raise an error if
		not.
		"""

		if( ongoing ):
			if self.imported_files is None:
				raise self.DeploymentOngoing( False )
		else:
			if self.imported_files is not None:
				raise self.DeploymentOngoing( True )


	def get_initial_revno( self ):
		"""Get the initial revision identifier from the deployment state.
		"""

		return self.initial_revno


	def copy_in( self, initial_revno = None ):
		"""Copy-in changes from the home directory to the repository.  When finished,
		the state of deployment is saved, meaning that a deployment is ongoing.
		"""

		# check we don't already have a file list
		self.check_ongoing( False )

		# if the repo doesn't exist, we have an empty file list
		if not os.path.exists( the.repo.full_dir ):
			self.imported_files = list()
		else:
			# copy in
			if the.verbose >= 1: print "importing files..."
			walker = CopyInWalker()
			walker.walk()
			self.imported_files = walker.walk_list

			# obtain initial revno
			self.initial_revno = the.repo.vcs.get_revno()

		# save state
		self.save_deployment_state()


	def get_conflicts( self, affected_files = None ):
		"""Check to see if there are any deployment conflicts.  If a list of affected
		files is supplied, then only those files are checked (and they are also
		saved with the deployment state).  Otherwise, all files in the
		repository are checked.
		"""

		# check we have a file list
		self.check_ongoing( True )

		# set updated files
		if affected_files is not None:
			self.affected_files = affected_files
			self.save_deployment_state()

		# check for deployment conflicts
		walker = ConflictWalker( self.imported_files, self.affected_files )
		walker.walk()

		self.conflicts_checked = True
		return walker.changed + walker.obstructed


	def copy_out( self ):
		"""Copy-out changed files from the repository to the home directory.  If the
		deployment state includes a list of affected files, then only those
		files are copied-out.
		"""

		# check we have a file list
		self.check_ongoing( True )

		# check that deployment conflicts have been checked-for
		if not self.conflicts_checked:
			raise RuntimeError('logic error: deployment conflicts unchecked' )

		# copy out
		if the.verbose >= 1: print "exporting files..."
		walker = CopyOutWalker( self.affected_files )
		walker.walk()

		# clear state
		self.remove_deployment_state()


	class DeploymentOngoing( the.program.FatalError ):

		def __init__( self, ongoing ):
			if( ongoing ):
				self.msg = "there is an ongoing deployment"
			else:
				self.msg = "there is no ongoing deployment"


	class CopyInConflicts( the.program.FatalError ):

		def __init__( self, conflicts ):
			self.conflicts = conflicts
