#!/usr/bin/env python
from shutil import copyfile
from shutil import copytree
from shutil import rmtree
from functools import reduce
import subprocess
import fnmatch
import pickle
import time
import sys
import os

## TODO:
## Add support for deleted classes

LAZY_DIR = os.path.dirname(__file__) + "/"
LOCAL_DIR = os.getcwd() + "/"
MODEL_DIR_NAME = "lazy_models"
MODEL_FILENAME = LOCAL_DIR.replace("/", "") + ".p"
MODEL_PATH = LAZY_DIR + MODEL_DIR_NAME + '/' + MODEL_FILENAME
TEMP_DIR_NAME = "temp"

PIPE = subprocess.PIPE

def printProgress(preamble, progress, segments):
	fillThresh = 1/segments
	fill = 0
	while progress > fillThresh * fill:
		fill += 1
	bar = "\r" + preamble + "[" + fill * "=" + (segments-fill) * " " + "]"
	print(bar, end="")

def execute(cmd):
    process = subprocess.Popen(cmd, stdout=PIPE, universal_newlines=True)
    for stdout_line in iter(process.stdout.readline, ""):
        yield stdout_line 
    process.stdout.close()
    process.wait()

def getCurrentCommit():
	process = subprocess.Popen(["git", "rev-parse", "--short", "HEAD"], stdout=PIPE, stderr=PIPE)
	stdoutput, stderroutput = process.communicate()
	return stdoutput.decode("utf-8")[:-1]

def getDiffFiles(lastHash):
	process = subprocess.Popen(["git", "diff", lastHash, "--name-only", "--diff-filter=AMB"], stdout=PIPE, stderr=PIPE)
	stdoutput, stderroutput = process.communicate()
	files = stdoutput.decode("utf-8")[:-1].split("\n")
	return files if files[0] != '' else []

def getUntrackedFiles():
	process = subprocess.Popen(["git", "ls-files", ".", "--exclude-standard", "--others"], stdout=PIPE, stderr=PIPE)
	stdoutput, stderroutput = process.communicate()
	files = stdoutput.decode("utf-8")[:-1].split("\n")
	return files if files[0] != '' else []

class LazyDeployer:
	def __init__(self):
		try:
			self.model = pickle.load(open(MODEL_PATH, "rb"))
		except:
			try:
				os.mkdir(LAZY_DIR + MODEL_DIR_NAME)
			except:
				pass
			self.intializeModel()

	def intializeModel(self):
		print("Initializing source tracking in " + LOCAL_DIR.split("/")[-2])
		self.model = {}
		self.model["FILES"] = set()
		self.model["FILE_CTIMES"] = {}
		self.model["LASTHASH"] = getCurrentCommit()
		pickle.dump(self.model, open(MODEL_PATH, "wb"))
		sys.exit(0)

	def deploy(self, reset):
		if reset:
			self.intializeModel()

		lastHash = self.model["LASTHASH"]
		lastFiles = self.model["FILES"]

		diffFileList = getDiffFiles(lastHash)
		untrackedFileList = getUntrackedFiles()
		uniqueFileList = list(set(untrackedFileList).union(set(diffFileList).union(set(lastFiles))))

		# ## Read forceignore
		patterns = ["**.json", ".*", "**.ts", "**.resource", "sdk-nwc", "**.apex", "**.sh", "*.installedPackage-meta.xml"]
		ignore = open(".forceignore", "r")
		ignorelines = ignore.readlines()
		for line in ignorelines:
			if "#" in line or len(line.strip()) == 0:
				continue
			patterns.append(line.strip())

		## Filter filelist by forceignore and last modified date
		files = []
		for file in uniqueFileList:
			isIgnored = False
			for pattern in patterns:
				if fnmatch.fnmatch(file, pattern) or file.split("/")[-1][0] == ".":
					isIgnored = True
					break
			modTime = 0
			try:
				modTime = os.path.getctime(file)
			except:
				try:
					self.model["FILES"].remove(file)
					continue
				except:
					continue
			if file in self.model["FILE_CTIMES"]:
				if modTime == self.model["FILE_CTIMES"][file]:
					continue
			self.model["FILE_CTIMES"][file] = modTime
			if not isIgnored:
				files.append(file)
				self.model["FILES"].add(file)

		if not len(files):
			print("No files changed.")
			sys.exit(0)

		## Create temporary directory
		os.mkdir(TEMP_DIR_NAME)

		## Copy files to temporary directory
		count = 0
		copyIssues = []
		copiedTrees = []
		for file in files:
			name = file.split("/")[-1]
			printProgress("Preparing files " + str(count) + "/" + str(len(files)) + " ", count/len(files), 30)
			count += 1

			if "staticresources" in file:
				index = file.index("staticresources") + 15
				if file[0:index] in copiedTrees:
					continue
				copiedTrees.append(file[0:index])
				copytree(file[0:index], "./" + TEMP_DIR_NAME + "/" + file[0:index])
				continue
			elif "object-meta.xml" in file:
				index = file.rindex("/")
				if file[0:index] in copiedTrees:
					continue
				copiedTrees.append(file[0:index])
				os.makedirs(os.path.dirname("./" + TEMP_DIR_NAME + "/" + file[0:len(name)-1]), exist_ok=True)
				copytree(file[0:index], "./" + TEMP_DIR_NAME + "/" + file[0:index])
				continue
			elif "field-meta.xml" in file:
				index = file[0:file.rindex("/")].rindex("/")
				if file[0:index] in copiedTrees:
					continue
				copiedTrees.append(file[0:index])
				os.makedirs(os.path.dirname("./" + TEMP_DIR_NAME + "/" + file[0:len(name)-1]), exist_ok=True)
				copytree(file[0:index], "./" + TEMP_DIR_NAME + "/" + file[0:index])
				continue
			try:
				copyfile(file, "./" + TEMP_DIR_NAME + "/" + name)
			except:
				copyIssues.append(name)
			if "-meta.xml" in file:
				try:
					copyfile(file[0:-8], "./" + TEMP_DIR_NAME + "/" + name[0:-8])
				except:
					pass
			else:
				try:
					copyfile(file + "-meta.xml", "./" + TEMP_DIR_NAME + "/" + name + "-meta.xml")
				except:
					pass
		if len(copyIssues) == 1:
			print("\rUnable to copy:" + " "*30 + "\n" + copyIssues[0])
		elif len(copyIssues):
			print("\rUnable to copy:" + " "*30 + reduce(lambda cur, next: "\n" + cur + "\n" + next, copyIssues))

		## Deploy temporary directory
		errors = ""
		for line in execute(["sfdx", "force:source:deploy", "-p", TEMP_DIR_NAME]):
			if "error " in line.lower() and not "Dependent class is invalid" in line:
				errors += line
			elif line[:3] == "***":
				print("\r" + line.strip() + " " * 30)
			else:
				print(line, end="")

		## Remove temporary directory
		rmtree(TEMP_DIR_NAME)

		if len(errors):
			print(errors.strip())
			sys.exit(0)

		## Save current commit as next seed if successful
		self.model["LASTHASH"] = getCurrentCommit()
		pickle.dump(self.model, open(MODEL_PATH, "wb"))
		sys.exit(0)

def main():
	reset = False
	if len(sys.argv) == 2 and sys.argv[1] == "-r":
		reset = True
	elif len(sys.argv) > 1:
		print("Invalid syntax. Run without args or with -r to reset cache.")
		sys.exit(0)
	deployer = LazyDeployer()
	deployer.deploy(reset)

if __name__ == "__main__":
	main()
